springboot+shiro+jwt实现登录
Posted 一步一高
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了springboot+shiro+jwt实现登录相关的知识,希望对你有一定的参考价值。
前些日子我曾经使用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("账号已被禁用,请联系管理员!"); }// 校验token是否超时失效 & 或者账号密码是否错误 if (!jwtTokenRefresh(token, username, sysUser.getPassWord())) { throw new AuthenticationException("Token失效请重新登录!"); }return new SimpleAuthenticationInfo(sysUser,token,ByteSource.Util.bytes(sysUser.getSalt()),getName()); } /** * JWTToken刷新生命周期 (解决用户一直在线操作,提供Token失效问题) * 1、登录成功后将用户的JWT生成的Token作为k、v存储到cache缓存里面(这时候k、v值一样) * 2、当该用户再次请求时,通过JWTFilter层层校验之后会进入到doGetAuthenticationInfo进行身份验证 * 3、当该用户这次请求JWTToken值还在生命周期内,则会通过重新PUT的方式k、v都为Token值,缓存中的token值生命周期时间重新计算(这时候k、v值一样) * 4、当该用户这次请求jwt生成的token值已经超时,但该token对应cache中的k还是存在,则表示该用户一直在操作只是JWT的token失效了,程序会给token对应的k映射的v值重新生成JWTToken并覆盖v值,该缓存生命周期重新计算 * 5、当该用户这次请求jwt在生成的token值已经超时,并在cache中不存在对应的k,则表示该用户账户空闲超时,返回用户信息已失效,请重新登录。 * 6、每次当返回为true情况下,都会给Response的Header中设置Authorization,该Authorization映射的v为cache对应的v值。 * 7、注:当前端接收到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) { log.info("jwtTokenRefresh参数:token="+token+",userName="+userName+",passWord="+passWord); String cacheToken = String.valueOf(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token));if (!Strings.isNullOrEmpty(cacheToken)) { // 校验token有效性 if (!JwtUtil.verify(cacheToken, userName, passWord)) { String newAuthorization = JwtUtil.sign(userName, passWord); redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization,JwtUtil.EXPIRE_TIME / 1000); } else { redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, cacheToken,JwtUtil.EXPIRE_TIME / 1000); } return true; } return false; } }
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_"; }