本章的设计思想是,所有登录都是无状态的,利用token在redis中认证,所以要关掉shiro的session,然后因为shiro在做权限校验的时候需要在subject(存在每一次请求的ThreadLocal里面)拿出用户信息,所以需要做一个登录信息的注入,不然就会报出用户未认证的异常,必须补充登录信息
权限系统——理解——shiro内置的数据封装
SimpleAuthenticationInfo
用于封装用户是身份信息+认证信息,认证信息是用于调用匹配器做检验的核心关键
public class SimpleAuthenticationInfo implements MergableAuthenticationInfo, SaltedAuthenticationInfo {
protected PrincipalCollection principals;//用户的身份信息,用于存储在本地拿出用户信息的数据
protected Object credentials;//用户的认证信息,用于校验合法
protected ByteSource credentialsSalt;//信息加密盐
/*
常用构造方法(1)
@principal:/用户的身份信息,用于存储在本地拿出用户信息的数据
@hashedCredentials:用户的认证信息,用于校验合法
@credentialsSalt:用户身份信息需要的盐
@realmName:每个reaml配置不同的,以便于用户信息根据reaml的不同分key存储
*/
public SimpleAuthenticationInfo(Object principal, Object hashedCredentials, ByteSource credentialsSalt, String realmName) {
this.principals = new SimplePrincipalCollection(principal, realmName);
this.credentials = hashedCredentials;
this.credentialsSalt = credentialsSalt;
}
/*
常用构造方法(2)
@principal:用户身份信息
@credentials:用户认证信息
@realmName:每个reaml配置不同的,以便于用户信息根据reaml的不同分key存储
*/
public SimpleAuthenticationInfo(Object principal, Object credentials, String realmName) {
this.principals = new SimplePrincipalCollection(principal, realmName);
this.credentials = credentials;
}
}
SimplePrincipalCollection
用户自己提交的身份信息
public class SimplePrincipalCollection implements MutablePrincipalCollection {
/*
不同Reaml,她的principal会用不同的key存起来,一个key对应一个set
*/
private Map<String, Set> realmPrincipals;
/*
针对数据类型做处理
*/
public SimplePrincipalCollection(Object principal, String realmName) {
if (principal instanceof Collection) {
addAll((Collection) principal, realmName);
} else {
add(principal, realmName);
}
}
/*
将数据添加进hashmap
*/
public void add(Object principal, String realmName) {
if (realmName == null) {
throw new IllegalArgumentException("realmName argument cannot be null.");
}
if (principal == null) {
throw new IllegalArgumentException("principal argument cannot be null.");
}
this.cachedToString = null;
getPrincipalsLazy(realmName).add(principal);
}
/*
懒加载hashMap
*/
protected Collection getPrincipalsLazy(String realmName) {
if (realmPrincipals == null) {
realmPrincipals = new LinkedHashMap<String, Set>();
}
Set principals = realmPrincipals.get(realmName);
if (principals == null) {
principals = new LinkedHashSet();
realmPrincipals.put(realmName, principals);
}
return principals;
}
}
权限系统——操作——整体配置
maven依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.1</version>
</dependency>
ShiroSessionManager
@Component("sessionManager")
public class ShiroSessionManager extends DefaultWebSessionManager {
/**
* @初始化
* 解决自定义sessionId
*/
public ShiroSessionManager(){
super.setGlobalSessionTimeout(43200000);
super.setSessionValidationSchedulerEnabled(true);
super.setDeleteInvalidSessions(true);
super.setSessionIdCookie(new SimpleCookie("sessionId"));
}
}
ShiroBaseConfig
@Configuration
public class ShiroBaseConfig {
/**
* @Shiro最终集成
* 加入shiroUserRealm
* 加入sessionManager
*/
@Bean(name="securityManager")
public SessionsSecurityManager securityManager(DefaultWebSessionManager sessionManager,
ShiroUserRealm shiroUserRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(shiroUserRealm);
securityManager.setSessionManager(sessionManager);
return securityManager;
}
/**
* @注册——Shiro专用的Aop增强器
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* @Spring开启Aop
*/
@Bean
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator();
autoProxyCreator.setProxyTargetClass(true);
return autoProxyCreator;
}
}
权限系统——操作——利用Filter完成token校验
/**
* @主要是token鉴权
* 主要替代shiro内部的登录状态验证机制,提前先完成验证
*/
@Slf4j
@WebFilter(urlPatterns = "/**")
@Configuration
@Order(-1)
public class TokenVaildFilter implements Filter {
public static final String swagger_url = "swagger";
private static final Set<String> ALLOWED_PATHS = Collections.unmodifiableSet(new HashSet<>(
Arrays.asList(
"/v1/login/mobilephonelogin",
"/v1/login/sendsms",
"/v2/api-docs",
"/public/getToken",
"/public/getApproveToken",
"/public/getTestToken"
)));
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
/**
* @(0)关闭shiro的session状态存储
*/
httpServletRequest.setAttribute(DefaultSubjectContext.SESSION_CREATION_ENABLED, Boolean.FALSE);
/**
* @(1)swageer请求或者白名单跳过
*/
String servletPath = httpServletRequest.getServletPath();
boolean allowedPath = ALLOWED_PATHS.contains(servletPath);
boolean isSwaggerUrl = servletPath.indexOf(swagger_url) > -1;
if (isSwaggerUrl ||!allowedPath){
filterChain.doFilter(httpServletRequest, servletResponse);
}
/**
* @(2)校验token,并将数据加入到threadLocal里面
*/
CombineUserInfo combineUserInfo = JwtUtil.parseToken(TokenUtils.getRequestToken(servletRequest));
if(JwtUtil.isVerify(combineUserInfo)){
ShiroUtil.createUserInfoOnThread(httpServletRequest,httpServletResponse,combineUserInfo.getUserInfo());
filterChain.doFilter(httpServletRequest, servletResponse);
}
ShiroUtil.failResponse(servletRequest,servletResponse);
}
@Override
public void destroy() {
ShiroUtil.destroyUserInfoOnThread();
}
}
权限系统——操作——利用Filter完成登录信息注入
需要重新shiro内置的token实体类,调用executeLogin方法才能用户信息合并到当前Subject中,用于权限校验时的基础判断
/**
* @shiro登录状态适配器
* shiro的session仅在登录机完成来的认证,在分布式环境下,其他机器则会丢失认证,我们进行权限校验的时候就会返回null
*/
@Order(Integer.MAX_VALUE)
public class ShiroLoginApadterFilter extends AuthenticatingFilter {
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
UserInfo userInfo = TokenUtils.getRequestUserInfo(request);
return new OAuth2Token(TokenUtils.getRequestToken(request), userInfo);
}
/**
* @登录状态特殊跳过,比如白名单
* 如果已经进入了这个,则证明token是合法的或者是白名单的url
* 返回false则会进入登录状态补充进入onAccessDenied
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
/**
* @白名单的跳过
*/
String token = TokenUtils.getRequestToken(request);
UserInfo userInfo = (UserInfo)request.getAttribute(UserInfo.class.getSimpleName());
if (StringUtils.isBlank(token) || userInfo == null) {
return true;
}
/**
* @需要进行登录状态数据的补充,最后调用onAccessDenied
*/
return false;
}
/**
* @登录状态数据的补充
* shiro权限验证时会首先判断用户是否登录
* 首先必须调用executeLogin,最后会调用createToken拿到用户信息完成状态数据的补充
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
boolean executeLogin = executeLogin(request, response);
if (!executeLogin) {
ShiroUtil.failResponse(request, response);
}
return executeLogin;
}
/**
* @登录失败则回调
* 返回一个二进制json数据给前端
*/
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
ShiroUtil.failResponse(request,response);
return false;
}
}
权限系统——操作——利用Realm 生成真实信息
@Slf4j
@Component
public class ShiroUserRealm extends AuthorizingRealm{
@Autowired
RedisUtil redisUtil;
/**
* @注入密码匹配器
* 由于身份认证已经在最前面的token拦截中,借用redis完成认证,所以直接调用空匹配器返回ture就不借用shiro完成认证了
* 如果是第一次登陆则用单独开启一个服务先匹配密码再生成token,也不借用shiro了
*/
public ShiroUserRealm(@Autowired ShiroEmptyCredentialMatcher shiroEmptyCredentialMatcher) {
super();
super.setCredentialsMatcher(shiroEmptyCredentialMatcher);
}
/**
* @生成认证信息的模板方法
* 先生成用户真实的完整信息,包括认证部分(一般是利用用户的token信息数据库查询)
* (1)用于调用匹配器匹配token中的认证信息和真实用户信息的比较
* (2)查出完整的用户身份信息,封装在本地缓存中
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UserInfo baseInputVO = (UserInfo) authenticationToken.getPrincipal();
String accessToken = (String) authenticationToken.getCredentials();
return new SimpleAuthenticationInfo(baseInputVO, accessToken, getName());
}
/**
* @生成权限信息 的模板方法
* 拿到用户所有的principalCollection,就是所有reaml的身份信息
* 用于身份信息去redis拿出所有菜单、角色
* 最后用于aop去匹配和比较
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo auth = new SimpleAuthorizationInfo();
UserInfo inputVO = (UserInfo) principals.getPrimaryPrincipal();
Set<String> roleId = null;
Set<String> auths = null;
if (inputVO != null) {
roleId = inputVO.getRoleId();
if (CollectionUtils.isEmpty(roleId)) {
return auth;
}
String authsCacheKey = AuthCacheKey.getAuthsCacheKey(inputVO.getCorpId());
Map<Object, Object> roleMenus = redisUtil.getHashEntries(authsCacheKey);
if (roleMenus.size() != 0) {
Set<String> finalRoleId = roleId;
auths = roleMenus.entrySet().stream()
.filter(map -> (finalRoleId.contains(map.getKey())))
.map(o -> o.getValue())
.flatMap(set -> ((HashSet<String>) set).stream())
.collect(Collectors.toSet());
} else {
ResponseVO<Set<String>> responseVO = sysAuthInfoRealmApi.findAllAuthByRole(inputVO);
if (responseVO != null) {
if (responseVO.getCode() == ResponseCode.OK.value()) {
auths = responseVO.getData();
}
}
}
}
auth.setRoles(roleId); // 保存所有的角色
auth.setStringPermissions(auths); // 保存所有的权限
return auth;
}
}
权限系统——操作——利用匹配器完成认证比较
Token已经完成合法验证,不需要匹配
@Component
public class ShiroEmptyCredentialMatcher extends SimpleCredentialsMatcher {
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
return true;
}
}
权限系统——工具类
工具——jwt工具类
工具类,用于解析和校验token的合法性
public class JwtUtil {
public static RedisUtil redisUtil;
public static String jwtSecretKey = "userJwt";
public static String tokenPrifix = "__tokenPrifix";
/**
* @签发token
* 生成一个clientToken可以运算得到有效载荷
*/
public static String createJWT(UserInfo user) {
long now = System.currentTimeMillis();
/**
* 【创建有效载荷】
* 并存人redis,设置过期时间作为token的失效时间
*/
String redisToken = user.getEmployeeName() + ":" + UUID.randomUUID().toString();
Map<String, Object> map = new HashMap<String, Object>();
map.put("employeeId", user.getEmployeeId());
map.put("employeeName", user.getEmployeeName());
map.put("employeeOrgCode", user.getEmployeeOrgCode());
map.put("employeeOrgName", user.getEmployeeOrgCode());
map.put("redisToken",redisToken);
redisUtil.setForTimeMS(tokenPrifix+user.getEmployeeId(),user,500000);//决定token有效时长
/**
* 【加密有效载荷】
* 最后生成一个可以摘要得到有效载荷的clientToken
*/
JwtBuilder builder = Jwts.builder()
.setClaims(map)//有效载荷
.setId(UUID.randomUUID().toString())//JWT的唯一标识,一次性token,从而回避重放攻击
.setIssuedAt(new Date(now))//签发时间
.setSubject(user.getEmployeeId().toString())//签发主体,用户唯一标识
.signWith(SignatureAlgorithm.HS256, jwtSecretKey);//签发算法+密钥
return builder.compact();
}
public static CombineUserInfo parseToken(String token) {
CombineUserInfo combineUserInfo =new CombineUserInfo();
try {
if(token==null){
return null;
}
Claims claims = Jwts.parser()//设置签名的秘钥
.setSigningKey(jwtSecretKey)//设置需要解析的jwt
.parseClaimsJws(token).getBody();
combineUserInfo.setClaims(claims);
combineUserInfo.setUserInfo(new UserInfo(){{
setEmployeeId((String) claims.get("employeeId"));
setEmployeeName((String) claims.get("employeeName"));
setEmployeeOrgCode((String) claims.get("employeeOrgCode"));
setEmployeeOrgName((String) claims.get("employeeOrgName"));
}});
return combineUserInfo ;
}catch (Exception e){
log.error(e.getMessage());
return combineUserInfo;
}
}
/**
* @校验token
* 1、拿到有效载荷中的redisToken再去redis里面拿最终数据
* 2、判断是不是空值、比较两边数据是否一致来判断token的有效性
*/
public static Boolean isVerify(Claims claims) {
/**
* @(1)redis真实数据获取
* 在有效载荷中取出redis数据作为对比基础,如果用是否是null做判断,可能会导致正好存在非当前用户的redis数据
*/
String redisToken = claims.get("redisToken").toString();
Object obj = redisUtil.get(redisToken);
if(obj==null){
throw new ApiException(401,"未登录");
}
/**
* @(2)token数据比对比redis数据
* 证明不是恰好算出一个合法的token,还必须一致匹配
*/
UserInfo userInfo=(UserInfo)obj;
if (claims.get("employeeId").equals(userInfo.getEmployeeId())) {
return true;
}
return false;
}
public static boolean isVerify(CombineUserInfo combineUserInfo) {
Claims claims = combineUserInfo.getClaims();
/**
* @(1)redis真实数据获取
* 在有效载荷中取出redis数据作为对比基础,如果用是否是null做判断,可能会导致正好存在非当前用户的redis数据
*/
String redisToken = claims.get("redisToken").toString();
Object obj = redisUtil.get(redisToken);
if(obj==null){
throw new ApiException(401,"未登录");
}
/**
* @(2)token数据比对比redis数据
* 证明不是恰好算出一个合法的token,还必须一致匹配
*/
UserInfo userInfo=(UserInfo)obj;
if (claims.get("employeeId").equals(userInfo.getEmployeeId())) {
return true;
}
return false;
}
}
同时封装两种数据
public class CombineUserInfo {
private UserInfo userInfo;
private Claims claims;
}
静态工具类注入spring里面的redisUitls、
@Component
public class StaticVarivaleProcesser {
@Autowired
RedisUtil redisUtil;
@PostConstruct
public void processStaticVarivaleRef() {
JwtUtil.redisUtil = redisUtil;
}
}
工具——token工具类
主要用于在取出requesy域中的原token和解析后的token
public class TokenUtils {
public static String getRequestToken(ServletRequest request) {
HttpServletRequest httpServletRequest=(HttpServletRequest)request;
String token = httpServletRequest.getHeader("Content-token");
if (StringUtils.isBlank(token)) {
token = httpServletRequest.getParameter("Content-token");
}
return token;
}
public static UserInfo getRequestUserInfo(ServletRequest request) {
HttpServletRequest httpServletRequest=(HttpServletRequest)request;
UserInfo userInfo = (UserInfo) httpServletRequest.getAttribute(UserInfo.class.getSimpleName());
return userInfo;
}
}
工具——shiro工具类
主要用于业务代码中取出request域里面的解析后的token
public class ShiroUtil {
public static void failResponse(ServletRequest request, ServletResponse response) {
response.setContentType("application/json;charset=utf-8");
try {
ResponseVO responseDTO = new ResponseVO(ResponseCodeEnum.UNLOGIN_EXCEPTION);
String json = JackSonUtil.serializeDTO(responseDTO);
response.getWriter().print(json);
} catch (Exception e1) {
e1.printStackTrace();
}
}
private static final ThreadLocal<ContextProvider> USER_INFO = new ThreadLocal<>();
/**
* @UserInfo相关方法
*/
public static UserInfo getUserInfo() {
ContextProvider contextProvider = (ContextProvider) USER_INFO.get();
if (contextProvider == null) {
throw new ApiException("没有使用@SessionInject注解,注入数据");
}
return contextProvider.getUserInfo();
}
/**
* @生命周期相关方法
*/
public static void createUserInfoOnThread(HttpServletRequest request, HttpServletResponse response, UserInfo userInfo) {
USER_INFO.set(new ContextProvider(request, response, userInfo));
}
public static void destroyUserInfoOnThread() {
USER_INFO.remove();
}
/**
* @Servlet相关方法
*/
public static HttpServletRequest getRequest() {
ContextProvider contextProvider = (ContextProvider) USER_INFO.get();
return contextProvider.getRequest();
}
public static HttpServletResponse getResponse() {
ContextProvider contextProvider = (ContextProvider) USER_INFO.get();
return contextProvider.getResponse();
}
}
封装存储在ThreadLocal中的数据
@Getter
class ContextProvider {
private HttpServletRequest request;
private HttpServletResponse response;
private UserInfo userInfo;
ContextProvider(HttpServletRequest request, HttpServletResponse response, UserInfo userInfo) {
this.request = request;
this.response = response;
this.userInfo = userInfo;
}
}
权限系统——源码疑惑
excuteLoign如何将登录信息合并到DelegatingSubject的PrincipalCollection principals中?
首先必须清楚Subject只存在ThreadLocal中,如果开启了有状态session认证则会存在于内存的MemorySessionDAO
这个操作主要考虑的将用户的AuthenticationToken转化成AuthenticationInfo,最后存进subject里面供权限认证的时候去用
详细分析可以看我的源码文章:https://blog.youkuaiyun.com/weixin_44275259/article/details/107939737
权限验证前,如何校验登录信息完整?
public class DelegatingSubject implements Subject {
/*
所有权限验证前都会调用assertAuthzCheckPossible要,验证用户登录数据是否完整
*/
public void checkPermission(String permission) throws AuthorizationException {
assertAuthzCheckPossible();
securityManager.checkPermission(getPrincipals(), permission);
}
protected void assertAuthzCheckPossible() throws AuthorizationException {
if (!hasPrincipals()) {
String msg = "This subject is anonymous - it does not have any identifying principals and " +
"authorization operations require an identity to check against. A Subject instance will " +
"acquire these identifying principals automatically after a successful login is performed " +
"be executing " + Subject.class.getName() + ".login(AuthenticationToken) or when 'Remember Me' " +
"functionality is enabled by the SecurityManager. This exception can also occur when a " +
"previously logged-in Subject has logged out which " +
"makes it anonymous again. Because an identity is currently not known due to any of these " +
"conditions, authorization is denied.";
throw new UnauthenticatedException(msg);
}
}
protected boolean hasPrincipals() {
return !isEmpty(getPrincipals());
}
}