1、前言
在微服務中咱們通常採用的是無狀態登陸,而傳統的session方式,在先後端分離的微服務架構下,如繼續使用則必將要解決跨域sessionId問題、集羣session共享問題等等。這顯然是費力不討好的,而整合shiro,卻很不恰巧的與咱們的指望有所違背:css
- shiro默認的攔截跳轉都是跳轉url頁面,而先後端分離後,後端並沒有權干涉頁面跳轉。
- shiro默認使用的登陸攔截校驗機制偏偏就是使用的session。
這固然不是咱們想要的,所以如需使用shiro,咱們就須要對其進行改造,那麼要如何改造呢?咱們能夠在整合shiro的基礎上自定義登陸校驗,繼續整合JWT,或者oauth2.0等,使其成爲支持服務端無狀態登陸,即token登陸。html
2、需求
- 首次經過post請求將用戶名與密碼到login進行登入;
- 登陸成功後返回token;
- 每次請求,客戶端需經過header將token帶回服務器作JWT Token的校驗;
- 服務端負責token生命週期的刷新
- 用戶權限的校驗;
3、實現
pom.xml
<!--shiro--> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring-boot-starter</artifactId> <version>1.4.0</version> </dependency> <!--JWT--> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.7.0</version> </dependency> <!-- Redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency>
ShiroConfig
/** * shiro 配置類 */ @Configuration public class ShiroConfig { /** * Filter Chain定義說明 * 一、一個URL能夠配置多個Filter,使用逗號分隔 * 二、當設置多個過濾器時,所有驗證經過,才視爲經過 * 三、部分過濾器可指定參數,如perms,roles */ @Bean("shiroFilter") public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); // 攔截器 Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>(); // 配置不會被攔截的連接 順序判斷 filterChainDefinitionMap.put("/sys/login", "anon"); //登陸接口排除 filterChainDefinitionMap.put("/sys/logout", "anon"); //登出接口排除 filterChainDefinitionMap.put("/", "anon"); filterChainDefinitionMap.put("/**/*.js", "anon"); filterChainDefinitionMap.put("/**/*.css", "anon"); filterChainDefinitionMap.put("/**/*.html", "anon"); filterChainDefinitionMap.put("/**/*.jpg", "anon"); filterChainDefinitionMap.put("/**/*.png", "anon"); filterChainDefinitionMap.put("/**/*.ico", "anon"); filterChainDefinitionMap.put("/druid/**", "anon"); filterChainDefinitionMap.put("/user/test", "anon"); //測試 // 添加本身的過濾器而且取名爲jwt Map<String, Filter> filterMap = new HashMap<String, Filter>(1); filterMap.put("jwt", new JwtFilter()); shiroFilterFactoryBean.setFilters(filterMap); // 過濾鏈定義,從上向下順序執行,通常將放在最爲下邊 filterChainDefinitionMap.put("/**", "jwt"); //未受權界面返回JSON shiroFilterFactoryBean.setUnauthorizedUrl("/sys/common/403"); shiroFilterFactoryBean.setLoginUrl("/sys/common/403"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } @Bean("securityManager") public DefaultWebSecurityManager securityManager(ShiroRealm myRealm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(myRealm); /* * 關閉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); return securityManager; } /** * 下面的代碼是添加註解支持 * * @return */ @Bean @DependsOn("lifecycleBeanPostProcessor") public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); defaultAdvisorAutoProxyCreator.setProxyTargetClass(true); return defaultAdvisorAutoProxyCreator; } @Bean public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } }
ShiroRealm
/** * 用戶登陸鑑權和獲取用戶受權 */ @Component @Slf4j public class ShiroRealm extends AuthorizingRealm { @Autowired @Lazy private ISysUserService sysUserService; @Autowired @Lazy private RedisUtil redisUtil; /** * 必須重寫此方法,否則Shiro會報錯 */ @Override public boolean supports(AuthenticationToken token) { return token instanceof JwtToken; } /** * 功能: 獲取用戶權限信息,包括角色以及權限。只有當觸發檢測用戶權限時纔會調用此方法,例如checkRole,checkPermission * * @param principals token * @return AuthorizationInfo 權限信息 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { log.info("————權限認證 [ roles、permissions]————"); SysUser sysUser = null; String username = null; if (principals != null) { sysUser = (SysUser) principals.getPrimaryPrincipal(); username = sysUser.getUserName(); } SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); // 設置用戶擁有的角色集合,好比「admin,test」 Set<String> roleSet = sysUserService.getUserRolesSet(username); info.setRoles(roleSet); // 設置用戶擁有的權限集合,好比「sys:role:add,sys:user:add」 Set<String> permissionSet = sysUserService.getUserPermissionsSet(username); info.addStringPermissions(permissionSet); return info; } /** * 功能: 用來進行身份認證,也就是說驗證用戶輸入的帳號和密碼是否正確,獲取身份驗證信息,錯誤拋出異常 * * @param auth 用戶身份信息 token * @return 返回封裝了用戶信息的 AuthenticationInfo 實例 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException { String token = (String) auth.getCredentials(); if (token == null) { log.info("————————身份認證失敗——————————IP地址: " + CommonUtils.getIpAddrByRequest(SpringContextUtils.getHttpServletRequest())); throw new AuthenticationException("token爲空!"); } // 校驗token有效性 SysUser loginUser = this.checkUserTokenIsEffect(token); return new SimpleAuthenticationInfo(loginUser, token, getName()); } /** * 校驗token的有效性 * * @param token */ public SysUser checkUserTokenIsEffect(String token) throws AuthenticationException { // 解密得到username,用於和數據庫進行對比 String username = JwtUtil.getUsername(token); if (username == null) { throw new AuthenticationException("token非法無效!"); } // 查詢用戶信息 SysUser loginUser = new SysUser(); SysUser sysUser = sysUserService.getUserByName(username); if (sysUser == null) { throw new AuthenticationException("用戶不存在!"); } // 校驗token是否超時失效 & 或者帳號密碼是否錯誤 if (!jwtTokenRefresh(token, username, sysUser.getPassWord())) { throw new AuthenticationException("Token失效請從新登陸!"); } // 判斷用戶狀態 if (!"0".equals(sysUser.getDelFlag())) { throw new AuthenticationException("帳號已被刪除,請聯繫管理員!"); } BeanUtils.copyProperties(sysUser, loginUser); return loginUser; } /** * JWTToken刷新生命週期 (解決用戶一直在線操做,提供Token失效問題) * 一、登陸成功後將用戶的JWT生成的Token做爲k、v存儲到cache緩存裏面(這時候k、v值同樣) * 二、當該用戶再次請求時,經過JWTFilter層層校驗以後會進入到doGetAuthenticationInfo進行身份驗證 * 三、當該用戶此次請求JWTToken值還在生命週期內,則會經過從新PUT的方式k、v都爲Token值,緩存中的token值生命週期時間從新計算(這時候k、v值同樣) * 四、當該用戶此次請求jwt生成的token值已經超時,但該token對應cache中的k仍是存在,則表示該用戶一直在操做只是JWT的token失效了,程序會給token對應的k映射的v值從新生成JWTToken並覆蓋v值,該緩存生命週期從新計算 * 五、當該用戶此次請求jwt在生成的token值已經超時,並在cache中不存在對應的k,則表示該用戶帳戶空閒超時,返回用戶信息已失效,請從新登陸。 * 六、每次當返回爲true狀況下,都會給Response的Header中設置Authorization,該Authorization映射的v爲cache對應的v值。 * 七、注:當前端接收到Response的Header中的Authorization值會存儲起來,做爲之後請求token使用 * 參考方案:https://blog.csdn.net/qq394829044/article/details/82763936 * * @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 (CommonUtils.isNotEmpty(cacheToken)) { // 校驗token有效性 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 / 1000); } else { redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, cacheToken); // 設置超時時間 redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME / 1000); } return true; } return false; } }
JwtFilter
/** * 鑑權登陸攔截器 **/ @Slf4j public class JwtFilter extends BasicHttpAuthenticationFilter { /** * 執行登陸認證 * * @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) { throw new AuthenticationException("Token失效請從新登陸"); } } /** * */ @Override protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest httpServletRequest = (HttpServletRequest) request; String token = httpServletRequest.getHeader(CommonConstant.ACCESS_TOKEN); JwtToken jwtToken = new JwtToken(token); // 提交給realm進行登入,若是錯誤他會拋出異常並被捕獲 getSubject(request, response).login(jwtToken); // 若是沒有拋出異常則表明登入成功,返回true return true; } /** * 對跨域提供支持 */ @Override protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest httpServletRequest = (HttpServletRequest) request; HttpServletResponse httpServletResponse = (HttpServletResponse) response; httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin")); httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE"); httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers")); // 跨域時會首先發送一個option請求,這裏咱們給option請求直接返回正常狀態 if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) { httpServletResponse.setStatus(HttpStatus.OK.value()); return false; } return super.preHandle(request, response); } }
JwtToken
package cn.gathub.entity; import org.apache.shiro.authc.AuthenticationToken; public class JwtToken implements AuthenticationToken { private static final long serialVersionUID = 1L; private String token; public JwtToken(String token) { this.token = token; } @Override public Object getPrincipal() { return token; } @Override public Object getCredentials() { return token; } }
JwtUtils
/** * JWT工具類 **/ public class JwtUtil { // 過時時間30分鐘 public static final long EXPIRE_TIME = 30 * 60 * 1000; /** * 校驗token是否正確 * * @param token 密鑰 * @param secret 用戶的密碼 * @return 是否正確 */ public static boolean verify(String token, String username, String secret) { try { // 根據密碼生成JWT效驗器 Algorithm algorithm = Algorithm.HMAC256(secret); JWTVerifier verifier = JWT.require(algorithm).withClaim("username", username).build(); // 效驗TOKEN DecodedJWT jwt = verifier.verify(token); return true; } catch (Exception exception) { return false; } } /** * 得到token中的信息無需secret解密也能得到 * * @return token中包含的用戶名 */ public static String getUsername(String token) { try { DecodedJWT jwt = JWT.decode(token); return jwt.getClaim("username").asString(); } catch (JWTDecodeException e) { return null; } } /** * 生成簽名,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); } /** * 根據request中的token獲取用戶帳號 * * @param request * @return * @throws Exception */ public static String getUserNameByToken(HttpServletRequest request) throws Exception { String accessToken = request.getHeader(CommonConstant.ACCESS_TOKEN); String username = getUsername(accessToken); if (CommonUtils.isEmpty(username)) { throw new Exception("未獲取到用戶"); } return username; } }
LoginController
@RestController @RequestMapping("/sys") @Slf4j public class LoginController { @Autowired private ISysUserService sysUserService; @Autowired private RedisUtil redisUtil; @RequestMapping(value = "/login", method = RequestMethod.POST) public Result<JSONObject> login(@RequestBody SysUser loginUser) throws Exception { Result<JSONObject> result = new Result<JSONObject>(); String username = loginUser.getUserName(); String password = loginUser.getPassWord(); //1. 校驗用戶是否有效 SysUser sysUser = sysUserService.getUserByName(username); result = sysUserService.checkUserIsEffective(sysUser); if (!result.isSuccess()) { return result; } //2. 校驗用戶名或密碼是否正確 String userpassword = PasswordUtil.encrypt(username, password, sysUser.getSalt()); String syspassword = sysUser.getPassWord(); if (!syspassword.equals(userpassword)) { result.error500("用戶名或密碼錯誤"); return result; } //用戶登陸信息 userInfo(sysUser, result); return result; } /** * 退出登陸 * * @param request * @param response * @return */ @RequestMapping(value = "/logout") public Result<Object> logout(HttpServletRequest request, HttpServletResponse response) { //用戶退出邏輯 String token = request.getHeader(CommonConstant.ACCESS_TOKEN); if (CommonUtils.isEmpty(token)) { return Result.error("退出登陸失敗!"); } String username = JwtUtil.getUsername(token); SysUser sysUser = sysUserService.getUserByName(username); if (sysUser != null) { log.info(" 用戶名: " + sysUser.getRealName() + ",退出成功! "); //清空用戶Token緩存 redisUtil.del(CommonConstant.PREFIX_USER_TOKEN + token); //清空用戶權限緩存:權限Perms和角色集合 redisUtil.del(CommonConstant.LOGIN_USER_CACHERULES_ROLE + username); redisUtil.del(CommonConstant.LOGIN_USER_CACHERULES_PERMISSION + username); return Result.ok("退出登陸成功!"); } else { return Result.error("無效的token"); } } /** * 用戶信息 * * @param sysUser * @param result * @return */ private Result<JSONObject> userInfo(SysUser sysUser, Result<JSONObject> result) { String syspassword = sysUser.getPassWord(); String username = sysUser.getUserName(); // 生成token String token = JwtUtil.sign(username, syspassword); redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, token); // 設置超時時間 redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME / 1000); // 獲取用戶部門信息 JSONObject obj = new JSONObject(); obj.put("token", token); obj.put("userInfo", sysUser); result.setResult(obj); result.success("登陸成功"); return result; } }
4、演示
使用正確的用戶名密碼進行登錄,登錄成功後返回token 使用錯誤的用戶名密碼進行登錄,登錄失敗 headers中攜帶正確的token訪問接口 headers中不攜帶token或者攜帶錯誤的token訪問接口 無權限的用戶訪問接口 無需登錄token也能夠訪問的接口(在過濾器中將接口或者資源文件放開) 前端
5、github源碼地址
地址:https://github.com/it-wwh/sping-boot-shiro-jwt-redis.gitjava
今天的更新到這裏就結束了,拜拜!!!git
感謝一路支持個人人,您的關注是我堅持更新的動力,有問題能夠在下面評論留言或隨時與我聯繫。。。。。。 QQ:850434439 微信:w850434439 EMAIL:gathub@qq.comgithub
若是有興趣和本博客交換友鏈的話,請按照下面的格式在評論區進行評論,我會盡快添加上你的連接。redis
網站名稱:GatHub-HongHui'S Blog 網站地址:https://gathub.cn 網站描述:不習慣的事愈來愈多,但我仍在前進…就算步伐很小,我也在一步一步的前進。 網站Logo/頭像:頭像地址spring
個人微信公衆號,歡迎你們來撩! 數據庫