大家好,我是程序员田同学。
公司开始了新项目,新项目的认证采用的是Shiro实现。由于涉及到多端登录用户,而且多端用户还是来自不同的表。
这就涉及到了Shiro的多realm,今天的demo主要是介绍Shiro的多realm实现方案,文中包含所有的代码,需要的朋友可以无缝copy。
前后端分离的背景下,在认证的实现中主要是两方面的内容,一个是用户登录获取到token,二是从请求头中拿到token并检验token的有效性和设置缓存。
1、用户登录获取token
登录和以往单realm实现逻辑一样,使用用户和密码生成token返回给前端,前端每次请求接口的时候携带token。
1 2 3 4 5 6 7 8 9 10 11 12 13
| @ApiOperation(value="登录", notes="登录") public Result<JSONObject> wxappLogin(String username,String password){ Result<JSONObject> result = new Result<JSONObject>(); JSONObject obj = new JSONObject();
String password="0"; String token = JwtUtil.sign(username, password); obj.put("token", token); result.setResult(obj); result.success("登录成功");
return result; }
|
生成token的工具类
1 2 3 4 5 6 7 8 9 10 11 12 13
|
public static String sign(String username, String secret) { Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME); Algorithm algorithm = Algorithm.HMAC256(secret); return JWT.create().withClaim("username", username).withExpiresAt(date).sign(algorithm); }
|
以上就实现了简单的登录逻辑,和Shiro的单realm设置和SpringSecurity的登录逻辑都没有什么区别。
2、鉴权登录拦截器(验证token有效性)
使用Shiro登录拦截器的只需要继承Shiro的 BasicHttpAuthenticationFilter 类 重写 isAccessAllowed()方法,在该方法中我们从ServletRequest中获取到token和login_type。
需要特别指出的是,由于是多realm,我们在请求头中加入一个login_type来区分不同的登录类型。
通过token和login_type我们生成一个JwtToken对象提交给getSubject。
JwtFilter过滤器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| @Slf4j public class JwtFilter extends BasicHttpAuthenticationFilter {
private boolean allowOrigin = true;
public JwtFilter(){} public JwtFilter(boolean allowOrigin){ this.allowOrigin = allowOrigin; }
@Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { try { executeLogin(request, response); return true; } catch (Exception e) { JwtUtil.responseError(response,401,CommonConstant.TOKEN_IS_INVALID_MSG); return false; } }
@Override protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest httpServletRequest = (HttpServletRequest) request; String token = httpServletRequest.getHeader(CommonConstant.X_ACCESS_TOKEN); String loginType = httpServletRequest.getHeader(CommonConstant.LOGIN_TYPE); if (oConvertUtils.isEmpty(token)) { token = httpServletRequest.getParameter("token"); }
JwtToken jwtToken = new JwtToken(token,loginType); getSubject(request, response).login(jwtToken); return true; } }
|
JwtToken类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| public class JwtToken implements AuthenticationToken { private static final long serialVersionUID = 1L; private String token;
private String loginType;
public JwtToken(String token,String loginType) { this.token = token; this.loginType=loginType; } public String getToken() { return token; }
public void setToken(String token) { this.token = token; }
public String getLoginType() { return loginType; }
public void setLoginType(String loginType) { this.loginType = loginType; }
@Override public Object getPrincipal() { return token; } @Override public Object getCredentials() { return token; } }
|
再往下的逻辑肯定会先根据我们的login_type来走不同的realm了,然后在各自的realm中去检查token的有效性了,那Shiro怎么知道我们的Realm都是哪些呢?
接下来就该引出使用Shiro的核心配置文件了——ShiroConfig.java类
shiro的配置文件中会注入名字为securityManager的Bean。
在该bean中首先注入ModularRealmAuthenticator,ModularRealmAuthenticator会根据配置的AuthenticationStrategy(身份验证策略)进行多Realm认证过程。
由于是多realm我们需要重写ModularRealmAuthenticator类,ModularRealmAuthenticator类中用于判断逻辑走不同的realm,接着注入我们的两个realm,分别是myRealm和clientShiroRealm。
重新注入 ModularRealm类
1 2 3 4 5 6 7
| @Bean public ModularRealm ModularRealm(){ ModularRealm modularRealm = new ModularRealm();
return modularRealm; }
|
securityManager-bean。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| @Bean("securityManager") public DefaultWebSecurityManager securityManager(ShiroRealm myRealm, ClientShiroRealm clientShiroRealm,ModularRealm modularRealm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setAuthenticator(modularRealm); List<Realm> realms = new ArrayList<>(); realms.add(myRealm); realms.add(clientShiroRealm); securityManager.setRealms(realms);
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO(); DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator(); defaultSessionStorageEvaluator.setSessionStorageEnabled(false); subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator); securityManager.setSubjectDAO(subjectDAO); securityManager.setCacheManager(redisCacheManager()); return securityManager; }
|
ModularRealm实现类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| public class ModularRealm extends ModularRealmAuthenticator {
@Override protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException { assertRealmsConfigured(); Collection<Realm> realms = getRealms(); HashMap<String, Realm> realmHashMap = new HashMap<>(realms.size());
for (Realm realm : realms) { realmHashMap.put(realm.getName(), realm); }
JwtToken token = (JwtToken) authenticationToken;
if (StrUtil.isEmpty(token.getLoginType())){ return doSingleRealmAuthentication(realmHashMap.get(LoginType.DEFAULT.getType()),token); } else { return doSingleRealmAuthentication(realmHashMap.get(token.getLoginType()),token); }
} }
|
然后会根据不同的login_type到不同的realm,下面为我的Shiro认证realm。
myrealm类.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
| @Component @Slf4j public class ShiroRealm extends AuthorizingRealm { @Lazy @Resource private CommonAPI commonApi;
@Lazy @Resource private RedisUtil redisUtil;
@Override public String getName() { return LoginType.DEFAULT.getType(); }
@Override public boolean supports(AuthenticationToken token) {
if (token instanceof JwtToken){ return StrUtil.isEmpty(((JwtToken) token).getLoginType()) || LoginType.CLIENT.getType().equals(((JwtToken) token).getLoginType()); } else { return false; } }
@Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { log.debug("===============Shiro权限认证开始============ [ roles、permissions]=========="); String username = null; if (principals != null) { LoginUser sysUser = (LoginUser) principals.getPrimaryPrincipal(); username = sysUser.getUsername(); } SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
Set<String> roleSet = commonApi.queryUserRoles(username); System.out.println(roleSet.toString()); info.setRoles(roleSet);
Set<String> permissionSet = commonApi.queryUserAuths(username); info.addStringPermissions(permissionSet); System.out.println(permissionSet); log.info("===============Shiro权限认证成功=============="); return info; }
@Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException { log.debug("===============Shiro身份认证开始============doGetAuthenticationInfo=========="); String token = (String) auth.getCredentials(); if (token == null) { HttpServletRequest req = SpringContextUtils.getHttpServletRequest(); log.info("————————身份认证失败——————————IP地址: "+ oConvertUtils.getIpAddrByRequest(req) +",URL:"+req.getRequestURI()); throw new AuthenticationException("token为空!"); } LoginUser loginUser = null; try { loginUser = this.checkUserTokenIsEffect(token); } catch (AuthenticationException e) { JwtUtil.responseError(SpringContextUtils.getHttpServletResponse(),401,e.getMessage()); e.printStackTrace(); return null; } return new SimpleAuthenticationInfo(loginUser, token, getName()); }
public LoginUser checkUserTokenIsEffect(String token) throws AuthenticationException { String username = JwtUtil.getUsername(token); if (username == null) { throw new AuthenticationException("token非法无效!"); }
log.debug("———校验token是否有效————checkUserTokenIsEffect——————— "+ token); LoginUser loginUser = TokenUtils.getLoginUser(username,commonApi,redisUtil); if (loginUser == null) { throw new AuthenticationException("用户不存在!"); } if (loginUser.getStatus() != 1) { throw new AuthenticationException("账号已被锁定,请联系管理员!"); } if (!jwtTokenRefresh(token, username, loginUser.getPassword())) { throw new AuthenticationException(CommonConstant.TOKEN_IS_INVALID_MSG); } String userTenantIds = loginUser.getRelTenantIds(); if(oConvertUtils.isNotEmpty(userTenantIds)){ String contextTenantId = TenantContext.getTenant(); String str ="0"; if(oConvertUtils.isNotEmpty(contextTenantId) && !str.equals(contextTenantId)){ String[] arr = userTenantIds.split(","); if(!oConvertUtils.isIn(contextTenantId, arr)){ throw new AuthenticationException("用户租户信息变更,请重新登陆!"); } } } return loginUser; }
public boolean jwtTokenRefresh(String token, String userName, String passWord) { String cacheToken = String.valueOf(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token)); if (oConvertUtils.isNotEmpty(cacheToken)) { if (!JwtUtil.verify(cacheToken, userName, passWord)) { String newAuthorization = JwtUtil.sign(userName, passWord); redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization); redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME *2 / 1000); log.debug("——————————用户在线操作,更新token保证不掉线—————————jwtTokenRefresh——————— "+ token); }
return true; }
return false; }
@Override public void clearCache(PrincipalCollection principals) { super.clearCache(principals); }
}
|
ClientShiroRealm类.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156
| @Component @Slf4j public class ClientShiroRealm extends AuthorizingRealm { @Lazy @Resource private ClientAPI clientAPI;
@Lazy @Resource private RedisUtil redisUtil; @Override public String getName() { return LoginType.CLIENT.getType(); }
@Override public boolean supports(AuthenticationToken token) {
if (token instanceof JwtToken){ return LoginType.CLIENT.getType().equals(((JwtToken) token).getLoginType()); } else { return false; } }
@Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { log.debug("===============Shiro权限认证开始============ [ roles、permissions]=========="); log.info("===============Shiro权限认证成功=============="); return null; }
@Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException { log.debug("===============Shiro身份认证开始============doGetAuthenticationInfo=========="); String token = (String) auth.getCredentials(); if (token == null) { HttpServletRequest req = SpringContextUtils.getHttpServletRequest(); log.info("————————身份认证失败——————————IP地址: "+ oConvertUtils.getIpAddrByRequest(req) +",URL:"+req.getRequestURI()); throw new AuthenticationException("token为空!"); } LoginUser loginUser = null; try { loginUser = this.checkUserTokenIsEffect(token); } catch (AuthenticationException e) { JwtUtil.responseError(SpringContextUtils.getHttpServletResponse(),401,e.getMessage()); e.printStackTrace(); return null; } return new SimpleAuthenticationInfo(loginUser, token, getName()); }
public LoginUser checkUserTokenIsEffect(String token) throws AuthenticationException { String username = JwtUtil.getUsername(token); if (username == null) { throw new AuthenticationException("token非法无效!"); }
log.debug("———校验token是否有效————checkUserTokenIsEffect——————— "+ token); LoginUser loginUser = TokenUtils.getClientLoginUser(username,clientAPI,redisUtil); if (loginUser == null) { throw new AuthenticationException("用户不存在!"); }
if (!jwtTokenRefresh(token, username, loginUser.getPassword())) { throw new AuthenticationException(CommonConstant.TOKEN_IS_INVALID_MSG); } return loginUser; }
public boolean jwtTokenRefresh(String token, String userName, String passWord) { String cacheToken = String.valueOf(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token)); if (oConvertUtils.isNotEmpty(cacheToken)) { if (!JwtUtil.verify(cacheToken, userName, passWord)) { String newAuthorization = JwtUtil.sign(userName, passWord); redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization); redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME *2 / 1000); log.debug("——————————用户在线操作,更新token保证不掉线—————————jwtTokenRefresh——————— "+ token); }
return true; }
return false; }
@Override public void clearCache(PrincipalCollection principals) { super.clearCache(principals); }
}
|
这两个realm更多的是需要实现我们自身的realm,我把我的全部代码贴上,读者可根据自己的需要进行修改,两个方法大致的作用都是检验token的有效性,只是查询的用户从不同的用户表中查出来的。
至此,Shiro的多Realm实现方案到这里就正式结束了。
安利时刻: