前些日子我曾經使用shiro來實現用戶的登錄,將賬號密碼托管給shiro,客戶端與服務端的連接通過cookie和session,
但是目前使用最多的登錄都是無狀態的,使用jwt或者oauth來實現登錄,所以也特地記錄一下。
1.第一步先添加jwt的依賴
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.7.0</version> </dependency>
2.修改shiro的配置,大體上沒有什么大的變化,主要就是關閉session和配置jwt到shiro中
@Bean public MyShiroRealm myShiroRealm(HashedCredentialsMatcher matcher){ MyShiroRealm myShiroRealm= new MyShiroRealm(); myShiroRealm.setCredentialsMatcher(matcher); return myShiroRealm; } @Bean public DefaultWebSecurityManager securityManager(HashedCredentialsMatcher matcher){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(myShiroRealm(matcher)); /* * 關閉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; } //如果沒有此name,將會找不到shiroFilter的Bean @Bean(name = "shiroFilter") public ShiroFilterFactoryBean shiroFilter(org.apache.shiro.mgt.SecurityManager securityManager){ ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); //shiroFilterFactoryBean.setLoginUrl("/login"); //表示指定登錄頁面 (前后分離不適用) //shiroFilterFactoryBean.setSuccessUrl("/user/list"); // 登錄成功后要跳轉的鏈接 (前后分離不適用) Map<String,String> filterChainDefinitionMap = new LinkedHashMap<>();//攔截器, 配置不會被攔截的鏈接 順序判斷 //filterChainDefinitionMap.put("/login","anon"); //所有匿名用戶均可訪問到Controller層的該方法下 filterChainDefinitionMap.put("/userLogin","anon"); filterChainDefinitionMap.put("/image/**","anon"); filterChainDefinitionMap.put("/css/**", "anon"); filterChainDefinitionMap.put("/fonts/**","anon"); filterChainDefinitionMap.put("/js/**","anon"); filterChainDefinitionMap.put("/logout","logout"); filterChainDefinitionMap.put("/**", "authc"); //authc:所有url都必須認證通過才可以訪問; anon:所有url都都可以匿名訪問 //filterChainDefinitionMap.put("/**", "user"); //user表示配置記住我或認證通過可以訪問的地址 // 添加自己的過濾器並且取名為jwt LinkedHashMap<String, Filter> filterMap = new LinkedHashMap<>(); filterMap.put("jwt", jwtFilter()); shiroFilterFactoryBean.setFilters(filterMap); // 過濾鏈定義,從上向下順序執行,一般將放在最為下邊 filterChainDefinitionMap.put("/**", "jwt"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } @Bean public JwtFilter jwtFilter() { return new JwtFilter(); } /** * SpringShiroFilter首先注冊到spring容器 * 然后被包裝成FilterRegistrationBean * 最后通過FilterRegistrationBean注冊到servlet容器 * @return */ @Bean public FilterRegistrationBean delegatingFilterProxy(){ FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(); DelegatingFilterProxy proxy = new DelegatingFilterProxy(); proxy.setTargetFilterLifecycle(true); proxy.setTargetBeanName("shiroFilter"); filterRegistrationBean.setFilter(proxy); return filterRegistrationBean; } @Bean(name = "hashedCredentialsMatcher") public HashedCredentialsMatcher hashedCredentialsMatcher() { HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); hashedCredentialsMatcher.setHashAlgorithmName("MD5"); hashedCredentialsMatcher.setHashIterations(1024);// 設置加密次數 return hashedCredentialsMatcher; } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(HashedCredentialsMatcher matcher) {//@Qualifier("hashedCredentialsMatcher") AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager(matcher)); return authorizationAttributeSourceAdvisor; } @Bean public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){ DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator(); defaultAdvisorAutoProxyCreator.setProxyTargetClass(true); return defaultAdvisorAutoProxyCreator; }
3.封裝token來替換Shiro原生Token,要實現AuthenticationToken接口
public class JwtToken implements AuthenticationToken { private static final long serialVersionUID = -8451637096112402805L; private String token; public JwtToken(String token) { this.token = token; } @Override public Object getPrincipal() { return token; } @Override public Object getCredentials() { return token; } }
4.添加一個JwtUtil的工具類來操作token
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); log.info(jwt+":-token is valid"); return true; } catch (Exception e) { log.info("The token is invalid{}",e.getMessage()); 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) { log.error("error:{}", e.getMessage()); 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); } }
5.寫一個攔截器JwtFilter,繼承BasicHttpAuthenticationFilter類
@Slf4j public class JwtFilter extends BasicHttpAuthenticationFilter { @Autowired private RedisUtil redisUtil; private AntPathMatcher antPathMatcher =new AntPathMatcher(); /** * 執行登錄認證(判斷請求頭是否帶上token) * @param request * @param response * @param mappedValue * @return */ @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { log.info("JwtFilter-->>>isAccessAllowed-Method:init()"); //如果請求頭不存在token,則可能是執行登陸操作或是游客狀態訪問,直接返回true if (isLoginAttempt(request, response)) { return true; } //如果存在,則進入executeLogin方法執行登入,檢查token 是否正確 try { executeLogin(request, response);return true; } catch (Exception e) { throw new AuthenticationException("Token失效請重新登錄"); } } /** * 判斷用戶是否是登入,檢測headers里是否包含token字段 */ @Override protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) { log.info("JwtFilter-->>>isLoginAttempt-Method:init()"); HttpServletRequest req = (HttpServletRequest) request; if(antPathMatcher.match("/userLogin",req.getRequestURI())){ return true; } String token = req.getHeader(CommonConstant.ACCESS_TOKEN); if (token == null) { return false; } Object o = redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token); if(ObjectUtils.isEmpty(o)){ return false; } log.info("JwtFilter-->>>isLoginAttempt-Method:返回true"); return true; } /** * 重寫AuthenticatingFilter的executeLogin方法丶執行登陸操作 */ @Override protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception { log.info("JwtFilter-->>>executeLogin-Method:init()"); HttpServletRequest httpServletRequest = (HttpServletRequest) request; String token = httpServletRequest.getHeader(CommonConstant.ACCESS_TOKEN);//Access-Token JwtToken jwtToken = new JwtToken(token); // 提交給realm進行登入,如果錯誤他會拋出異常並被捕獲, 反之則代表登入成功,返回true getSubject(request, response).login(jwtToken);return true; } /** * 對跨域提供支持 */ @Override protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception { log.info("JwtFilter-->>>preHandle-Method:init()"); 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); } }
6.修改自定義的Realm
public class MyShiroRealm extends AuthorizingRealm { @Autowired private RoleService roleService; @Autowired private UserService userService; @Autowired private PermissionService permissionService; @Autowired private RedisUtil redisUtil; /** * 必須重寫此方法,不然Shiro會報錯 */ @Override public boolean supports(AuthenticationToken token) { return token instanceof JwtToken; } /** * 訪問控制。比如某個用戶是否具有某個操作的使用權限 * @param principalCollection * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); User user = (User) principalCollection.getPrimaryPrincipal();if (user == null) { log.error("授權失敗,用戶信息為空!!!"); return null; } try { //獲取用戶角色集 Set<String> listRole= roleService.findRoleByUsername(user.getUserName()); simpleAuthorizationInfo.addRoles(listRole); //通過角色獲取權限集 for (String role : listRole) { Set<String> permission= permissionService.findPermissionByRole(role); simpleAuthorizationInfo.addStringPermissions(permission); } return simpleAuthorizationInfo; } catch (Exception e) { log.error("授權失敗,請檢查系統內部錯誤!!!", e); } return simpleAuthorizationInfo; } /** * 用戶身份識別(登錄") * @param authenticationToken * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { String token = (String) authenticationToken.getCredentials();// 校驗token有效性 String username = JwtUtil.getUsername(token);if (Strings.isNullOrEmpty(username)) { throw new AuthenticationException("token非法無效!"); }// 查詢用戶信息 User sysUser = userService.selectUserOne(username); if (sysUser == null) { throw new AuthenticationException("用戶不存在!"); }// 判斷用戶狀態 if (sysUser.getValid()==0) { throw new AuthenticationException("賬號已被禁用,請聯系管理員!"); }
return new SimpleAuthenticationInfo(sysUser,token,ByteSource.Util.bytes(sysUser.getSalt()),getName()); } }
7.登錄接口修改
public class LoginController { @Autowired private UserMapper userMapper; @Autowired private RedisUtil redisUtil; /** * 登錄 * @return */ @PostMapping(value = "/userLogin") @ResponseBody public Result<JSONObject> toLogin(@RequestBody User loginUser) throws Exception { Result<JSONObject> result = new Result<>(); String userName = loginUser.getUserName(); String passWord = loginUser.getPassWord(); User user=userMapper.selectUserOne(userName); if (user == null) { return result.error500("該用戶不存在"); } if (user.getValid()==0) { return result.error500("賬號已被禁用,請聯系管理員!"); }
//我的密碼是使用uuid作為鹽值加密的,所以這里登陸時候還需要做一次對比 SimpleHash simpleHash = new SimpleHash("MD5", passWord, user.getSalt(), 1024); if(!simpleHash.toHex().equals(user.getPassWord())){ return result.error500("密碼不正確"); } // 生成token String token = JwtUtil.sign(userName, passWord); redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, token,JwtUtil.EXPIRE_TIME / 1000); JSONObject obj = new JSONObject(); obj.put("token", token); obj.put("userInfo", user); result.setResult(obj); result.success("登錄成功"); return result; } }
添加的方法,這里的加密算法和加密次數以及鹽值都要一致,否則登錄時候密碼對比會失敗
@RequestMapping("/insertUser") @ResponseBody public int insertUser(User user){ //將uuid設置為密碼鹽值 String salt = UUID.randomUUID().toString().replaceAll("-",""); SimpleHash simpleHash = new SimpleHash("MD5", user.getPassWord(), salt, 1024); user.setPassWord(simpleHash.toHex()).setValid(1).setSalt(salt).setCreateTime(new Date()).setDel(0); return userMapper.insertSelective(user); }
定義的常量
public class CommonConstant { /** * 刪除標志 1 未刪除 0 */ public static final Integer DEL_FLAG_1 = 1; public static final Integer DEL_FLAG_0 = 0; public static final Integer SC_INTERNAL_SERVER_ERROR_500 = 500; public static final Integer SC_OK_200 = 200; /** * 訪問權限認證未通過 510 */ public static final Integer SC_JEECG_NO_AUTHZ = 510; /** * 登錄用戶令牌緩存KEY前綴 */ public static final int TOKEN_EXPIRE_TIME = 3600; //3600秒即是一小時 public static final String PREFIX_USER_TOKEN = "PREFIX_USER_TOKEN_"; /** * 0:一級菜單 */ public static final Integer MENU_TYPE_0 = 0; /** * 1:子菜單 */ public static final Integer MENU_TYPE_1 = 1; /** * 2:按鈕權限 */ public static final Integer MENU_TYPE_2 = 2; /** * 是否用戶已被凍結 1(解凍)正常 2凍結 */ public static final Integer USER_UNFREEZE = 1; public static final Integer USER_FREEZE = 2; /** * token的key */ public static String ACCESS_TOKEN = "Access-Token"; /** * 登錄用戶規則緩存 */ public static final String LOGIN_USER_RULES_CACHE = "loginUser_cacheRules"; /** * 登錄用戶擁有角色緩存KEY前綴 */ public static String LOGIN_USER_CACHERULES_ROLE = "loginUser_cacheRules::Roles_"; /** * 登錄用戶擁有權限緩存KEY前綴 */ public static String LOGIN_USER_CACHERULES_PERMISSION = "loginUser_cacheRules::Permissions_"; }