大家好,我是程序员田同学。
公司开始了新我的项目,新我的项目的认证采纳的是Shiro实现。因为波及到多端登录用户,而且多端用户还是来自不同的表。
这就波及到了Shiro的多realm,明天的demo次要是介绍Shiro的多realm实现计划,文中蕴含所有的代码,须要的敌人能够无缝copy。
前后端拆散的背景下,在认证的实现中次要是两方面的内容,一个是用户登录获取到token,二是从申请头中拿到token并测验token的有效性和设置缓存。
1、用户登录获取token
登录和以往单realm实现逻辑一样,应用用户和明码生成token返回给前端,前端每次申请接口的时候携带token。
@ApiOperation(value="登录", notes="登录") public Result<JSONObject> wxappLogin(String username,String password){ Result<JSONObject> result = new Result<JSONObject>(); JSONObject obj = new JSONObject(); // 生成token String password="0"; String token = JwtUtil.sign(username, password); obj.put("token", token); result.setResult(obj); result.success("登录胜利"); return result; }
生成token的工具类
/** * 生成签名,5min后过期 * * @param username 用户名 * @param secret 用户的明码 * @return 加密的token */public static String sign(String username, String secret) { Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME); Algorithm algorithm = Algorithm.HMAC256(secret); // 附带username信息 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过滤器
@Slf4jpublic class JwtFilter extends BasicHttpAuthenticationFilter { /** * 默认开启跨域设置(应用单体) */ private boolean allowOrigin = true; public JwtFilter(){} public JwtFilter(boolean allowOrigin){ this.allowOrigin = allowOrigin; } /** * 执行登录认证 * * @param request * @param response * @param mappedValue * @return */ @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; //throw new AuthenticationException("Token生效,请从新登录", e); } } /** * */ @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); // update-begin--Author:lvdandan Date:20210105 for:JT-355 OA聊天增加token验证,获取token参数 if (oConvertUtils.isEmpty(token)) { token = httpServletRequest.getParameter("token"); } // update-end--Author:lvdandan Date:20210105 for:JT-355 OA聊天增加token验证,获取token参数 JwtToken jwtToken = new JwtToken(token,loginType); // 提交给realm进行登入,如果谬误他会抛出异样并被捕捉 getSubject(request, response).login(jwtToken); // 如果没有抛出异样则代表登入胜利,返回true return true; } }
JwtToken类
public class JwtToken implements AuthenticationToken { private static final long serialVersionUID = 1L; private String token; private String loginType;// public JwtToken(String token) {// this.token = token;// } 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类
@Bean public ModularRealm ModularRealm(){ //本人重写的ModularRealmAuthenticator ModularRealm modularRealm = new ModularRealm();// modularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());//这里为默认策略:如果有一个或多个Realm验证胜利,所有的尝试都被认为是胜利的,如果没有一个验证胜利,则该次尝试失败 return modularRealm; }
securityManager-bean。
@Bean("securityManager") public DefaultWebSecurityManager securityManager(ShiroRealm myRealm, ClientShiroRealm clientShiroRealm,ModularRealm modularRealm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();// securityManager.setRealm(myRealm); securityManager.setAuthenticator(modularRealm); List<Realm> realms = new ArrayList<>(); //增加多个Realm realms.add(myRealm); realms.add(clientShiroRealm); securityManager.setRealms(realms); /* * 敞开shiro自带的session,详情见文档 * http://shiro.apache.org/session-management.html#SessionManagement- * StatelessApplications%28Sessionless%29 */ DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO(); DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator(); defaultSessionStorageEvaluator.setSessionStorageEnabled(false); subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator); securityManager.setSubjectDAO(subjectDAO); //自定义缓存实现,应用redis securityManager.setCacheManager(redisCacheManager()); return securityManager; }
ModularRealm实现类
public class ModularRealm extends ModularRealmAuthenticator { @Override protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException { assertRealmsConfigured(); Collection<Realm> realms = getRealms(); // 登录类型对应的所有Realm HashMap<String, Realm> realmHashMap = new HashMap<>(realms.size()); for (Realm realm : realms) { // 这里应用的realm中定义的Name属性来进行辨别,留神realm中要加上 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); }// return super.doAuthenticate(authenticationToken); }}
而后会依据不同的login_type到不同的realm,上面为我的Shiro认证realm。
myrealm类.
@Component@Slf4jpublic class ShiroRealm extends AuthorizingRealm { @Lazy @Resource private CommonAPI commonApi; @Lazy @Resource private RedisUtil redisUtil; @Override public String getName() { return LoginType.DEFAULT.getType(); } /** * 必须重写此办法,不然Shiro会报错 */ @Override public boolean supports(AuthenticationToken token) {// return token instanceof JwtToken; if (token instanceof JwtToken){ return StrUtil.isEmpty(((JwtToken) token).getLoginType()) || LoginType.CLIENT.getType().equals(((JwtToken) token).getLoginType()); } else { return false; } } /** * 权限信息认证(包含角色以及权限)是用户拜访controller的时候才进行验证(redis存储的此处权限信息) * 触发检测用户权限时才会调用此办法,例如checkRole,checkPermission * * @param principals 身份信息 * @return AuthorizationInfo 权限信息 */ @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(); // 设置用户领有的角色汇合,比方“admin,test” Set<String> roleSet = commonApi.queryUserRoles(username); System.out.println(roleSet.toString()); info.setRoles(roleSet); // 设置用户领有的权限汇合,比方“sys:role:add,sys:user:add” Set<String> permissionSet = commonApi.queryUserAuths(username); info.addStringPermissions(permissionSet); System.out.println(permissionSet); log.info("===============Shiro权限认证胜利=============="); return info; } /** * 用户信息认证是在用户进行登录的时候进行验证(不存redis) * 也就是说验证用户输出的账号和明码是否正确,谬误抛出异样 * * @param auth 用户登录的账号密码信息 * @return 返回封装了用户信息的 AuthenticationInfo 实例 * @throws AuthenticationException */ @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为空!"); } // 校验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()); } /** * 校验token的有效性 * * @param token */ public LoginUser checkUserTokenIsEffect(String token) throws AuthenticationException { // 解密取得username,用于和数据库进行比照 String username = JwtUtil.getUsername(token); if (username == null) { throw new AuthenticationException("token非法有效!"); } // 查问用户信息 log.debug("———校验token是否无效————checkUserTokenIsEffect——————— "+ token); LoginUser loginUser = TokenUtils.getLoginUser(username,commonApi,redisUtil); //LoginUser loginUser = commonApi.getUserByName(username); if (loginUser == null) { throw new AuthenticationException("用户不存在!"); } // 判断用户状态 if (loginUser.getStatus() != 1) { throw new AuthenticationException("账号已被锁定,请分割管理员!"); } // 校验token是否超时生效 & 或者账号密码是否谬误 if (!jwtTokenRefresh(token, username, loginUser.getPassword())) { throw new AuthenticationException(CommonConstant.TOKEN_IS_INVALID_MSG); } //update-begin-author:taoyan date:20210609 for:校验用户的tenant_id和前端传过来的是否统一 String userTenantIds = loginUser.getRelTenantIds(); if(oConvertUtils.isNotEmpty(userTenantIds)){ String contextTenantId = TenantContext.getTenant(); String str ="0"; if(oConvertUtils.isNotEmpty(contextTenantId) && !str.equals(contextTenantId)){ //update-begin-author:taoyan date:20211227 for: /issues/I4O14W 用户租户信息变更判断破绽 String[] arr = userTenantIds.split(","); if(!oConvertUtils.isIn(contextTenantId, arr)){ throw new AuthenticationException("用户租户信息变更,请从新登陆!"); } //update-end-author:taoyan date:20211227 for: /issues/I4O14W 用户租户信息变更判断破绽 } } //update-end-author:taoyan date:20210609 for:校验用户的tenant_id和前端传过来的是否统一 return loginUser; } /** * JWTToken刷新生命周期 (实现: 用户在线操作不掉线性能) * 1、登录胜利后将用户的JWT生成的Token作为k、v存储到cache缓存外面(这时候k、v值一样),缓存有效期设置为Jwt无效工夫的2倍 * 2、当该用户再次申请时,通过JWTFilter层层校验之后会进入到doGetAuthenticationInfo进行身份验证 * 3、当该用户这次申请jwt生成的token值曾经超时,但该token对应cache中的k还是存在,则示意该用户始终在操作只是JWT的token生效了,程序会给token对应的k映射的v值从新生成JWTToken并笼罩v值,该缓存生命周期从新计算 * 4、当该用户这次申请jwt在生成的token值曾经超时,并在cache中不存在对应的k,则示意该用户账户闲暇超时,返回用户信息已生效,请从新登录。 * 留神: 前端申请Header中设置Authorization放弃不变,校验有效性以缓存中的token为准。 * 用户过期工夫 = Jwt无效工夫 * 2。 * * @param userName * @param passWord * @return */ public boolean jwtTokenRefresh(String token, String userName, String passWord) { String cacheToken = String.valueOf(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token)); if (oConvertUtils.isNotEmpty(cacheToken)) { // 校验token有效性 if (!JwtUtil.verify(cacheToken, userName, passWord)) { //生成token 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); } //update-begin--Author:scott Date:20191005 for:解决每次申请,都重写redis中 token缓存问题// else {// // 设置超时工夫// redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, cacheToken);// redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME / 1000);// } //update-end--Author:scott Date:20191005 for:解决每次申请,都重写redis中 token缓存问题 return true; } //redis中不存在此TOEKN,阐明token非法返回false return false; } /** * 革除以后用户的权限认证缓存 * * @param principals 权限信息 */ @Override public void clearCache(PrincipalCollection principals) { super.clearCache(principals); }}
ClientShiroRealm类.
@Component@Slf4jpublic class ClientShiroRealm extends AuthorizingRealm { @Lazy @Resource private ClientAPI clientAPI; @Lazy @Resource private RedisUtil redisUtil; @Override public String getName() { return LoginType.CLIENT.getType(); } /** * 必须重写此办法,不然Shiro会报错 */ @Override public boolean supports(AuthenticationToken token) {// return token instanceof JwtToken; if (token instanceof JwtToken){ return LoginType.CLIENT.getType().equals(((JwtToken) token).getLoginType()); } else { return false; } } /** * 权限信息认证(包含角色以及权限)是用户拜访controller的时候才进行验证(redis存储的此处权限信息) * 触发检测用户权限时才会调用此办法,例如checkRole,checkPermission * * @param principals 身份信息 * @return AuthorizationInfo 权限信息 */ @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(); //// 设置用户领有的角色汇合,比方“admin,test” //Set<String> roleSet = commonApi.queryUserRoles(username); //System.out.println(roleSet.toString()); //info.setRoles(roleSet); // //// 设置用户领有的权限汇合,比方“sys:role:add,sys:user:add” //Set<String> permissionSet = commonApi.queryUserAuths(username); //info.addStringPermissions(permissionSet); //System.out.println(permissionSet); log.info("===============Shiro权限认证胜利=============="); return null; } /** * 用户信息认证是在用户进行登录的时候进行验证(不存redis) * 也就是说验证用户输出的账号和明码是否正确,谬误抛出异样 * * @param auth 用户登录的账号密码信息 * @return 返回封装了用户信息的 AuthenticationInfo 实例 * @throws AuthenticationException */ @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为空!"); } // 校验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()); } /** * 校验token的有效性 * * @param token */ public LoginUser checkUserTokenIsEffect(String token) throws AuthenticationException { // 解密取得username,用于和数据库进行比照 String username = JwtUtil.getUsername(token); if (username == null) { throw new AuthenticationException("token非法有效!"); } // 查问用户信息 log.debug("———校验token是否无效————checkUserTokenIsEffect——————— "+ token); LoginUser loginUser = TokenUtils.getClientLoginUser(username,clientAPI,redisUtil); //LoginUser loginUser = commonApi.getUserByName(username); if (loginUser == null) { throw new AuthenticationException("用户不存在!"); } // 校验token是否超时生效 & 或者账号密码是否谬误 if (!jwtTokenRefresh(token, username, loginUser.getPassword())) { throw new AuthenticationException(CommonConstant.TOKEN_IS_INVALID_MSG); } return loginUser; } /** * JWTToken刷新生命周期 (实现: 用户在线操作不掉线性能) * 1、登录胜利后将用户的JWT生成的Token作为k、v存储到cache缓存外面(这时候k、v值一样),缓存有效期设置为Jwt无效工夫的2倍 * 2、当该用户再次申请时,通过JWTFilter层层校验之后会进入到doGetAuthenticationInfo进行身份验证 * 3、当该用户这次申请jwt生成的token值曾经超时,但该token对应cache中的k还是存在,则示意该用户始终在操作只是JWT的token生效了,程序会给token对应的k映射的v值从新生成JWTToken并笼罩v值,该缓存生命周期从新计算 * 4、当该用户这次申请jwt在生成的token值曾经超时,并在cache中不存在对应的k,则示意该用户账户闲暇超时,返回用户信息已生效,请从新登录。 * 留神: 前端申请Header中设置Authorization放弃不变,校验有效性以缓存中的token为准。 * 用户过期工夫 = Jwt无效工夫 * 2。 * * @param userName * @param passWord * @return */ public boolean jwtTokenRefresh(String token, String userName, String passWord) { String cacheToken = String.valueOf(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token)); if (oConvertUtils.isNotEmpty(cacheToken)) { // 校验token有效性 if (!JwtUtil.verify(cacheToken, userName, passWord)) { //生成token 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; } //redis中不存在此TOEKN,阐明token非法返回false return false; } /** * 革除以后用户的权限认证缓存 * * @param principals 权限信息 */ @Override public void clearCache(PrincipalCollection principals) { super.clearCache(principals); }}
这两个realm更多的是须要实现咱们本身的realm,我把我的全副代码贴上,读者可依据本人的须要进行批改,两个办法大抵的作用都是测验token的有效性,只是查问的用户从不同的用户表中查出来的。
至此,Shiro的多Realm实现计划到这里就正式完结了。