springboot |
您所在的位置:网站首页 › springboot前后端分离登录 › springboot |
springboot-shiro-jwt-redis实现用户登录的认证与授权(前后端分离)
shiro-jwt-redis实现用户认证、授权大致流程
认证时进行缓存获取数据,否则进入认证方法(可以自己debug弄清流程更好)
相关依赖: 主要依赖: org.crazycake shiro-redis-spring-boot-starter 3.2.1 com.auth0 java-jwt 3.10.3代码中使用的工具依赖: com.alibaba fastjson 1.2.35避免启动失败: 在META-INF下创建spring-devtoos.propertis配置文件在里面编写 restart.include.shiro-redis=/shiro-[\\w-\\.]+jarapplication.yml配置 # DataSource Config spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/vueblog?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 mybatis-plus: mapper-locations: classpath*:/mapper/**Mapper.xml server: port: 8081 logging: #sql日志 level: com.xiaoke.mapper: DEBUG #dao接口全限定名称 shiro-redis: enabled: true redis-manager: host: 127.0.0.1:6379JwtUtils :生成token,以及判断token进行封装的工具包 package com.xiaoke.util.model;//写自己的包 import com.alibaba.fastjson.JSONObject; import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTDecodeException; import com.auth0.jwt.interfaces.DecodedJWT; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import java.io.UnsupportedEncodingException; import java.util.Date; import java.util.HashMap; import java.util.Map; // jwt工具类 @Slf4j @Data //自动生成setter、getter方法,可以直接写 @Component public class JwtUtils { //token有效时长 private static final long EXPIRE=30*60*1000L; //token的密钥 private static final String SECRET="jwt+shiro"; public static String createToken(JSONObject user) throws UnsupportedEncodingException { //token过期时间 Date date=new Date(System.currentTimeMillis()+EXPIRE); //jwt的header部分 Map map=new HashMap(); map.put("alg","HS256"); map.put("typ","JWT"); //使用jwt的api生成token String token= JWT.create() .withHeader(map) .withClaim("username", user.getString("username"))//私有声明 .withExpiresAt(date)//过期时间 .withIssuedAt(new Date())//签发时间 .sign(Algorithm.HMAC256(SECRET));//签名 return token; } //校验token的有效性,1、token的header和payload是否没改过;2、没有过期 public static boolean verify(String token){ try { //解密 JWTVerifier verifier=JWT.require(Algorithm.HMAC256(SECRET)).build(); verifier.verify(token); return true; }catch (Exception e){ return false; } } //无需解密也可以获取token的信息 public static String getUsername(String token){ try { DecodedJWT jwt = JWT.decode(token); return jwt.getClaim("username").asString(); } catch (JWTDecodeException e) { return null; } } }JwtToken:用来创建token信息 package com.xiaoke.config.shiro; import org.apache.shiro.authc.AuthenticationToken; public class JwtToken implements AuthenticationToken { private String token; public JwtToken(String jwt){ this.token = jwt; } @Override public Object getPrincipal() { return token; } @Override public Object getCredentials() { return token; } }JwtFilter:用来处理拦截后的请求 package com.xiaoke.config.shiro; import com.baomidou.mybatisplus.core.toolkit.StringUtils; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.web.filter.authc.AuthenticatingFilter; import org.springframework.stereotype.Component; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; @Component public class JwtFilter extends AuthenticatingFilter { @Override//重写token的创建,在执行onAccessDenied中的executeLogin时调用 protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception { HttpServletRequest request = (HttpServletRequest) servletRequest; String jwt = request.getHeader("Authorization"); if(StringUtils.isEmpty(jwt)){ return null; } return new JwtToken(jwt); } @Override//onPreHandler中调用判断执行 protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception { System.out.println("ServletRequest"); HttpServletRequest request= (HttpServletRequest) servletRequest; String token=request.getHeader("Authorization"); if(StringUtils.isEmpty(token)) {//mybatis-plus中工具包,也可以自行判断 System.out.println("ServletRequest::true"); return true; }else { try { executeLogin(servletRequest, servletResponse);//调用Subject中login return true; }catch (Exception e){ return false; } } } }AccountProfile:用来处理redis必须获取id做key package com.xiaoke.config.shiro; import lombok.Data; import java.io.Serializable; @Data public class AccountProfile implements Serializable { private Long id; private String username; private String email; public AccountProfile() { } public AccountProfile(Long id, String username, String email) { this.id = id; this.username = username; this.email = email; } }AccountRealm:进行认证、授权的自定义重写 package com.xiaoke.config.shiro; import com.alibaba.fastjson.JSONObject; import com.xiaoke.service.PermissionService; import com.xiaoke.service.UserService; import com.xiaoke.util.model.JwtUtils; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.util.List; @Component public class AccountRealm extends AuthorizingRealm { @Resource JwtUtils jwtUtils; @Resource UserService userService; @Resource PermissionService permissionService; //为了让realm支持jwt的凭证校验 @Override public boolean supports(AuthenticationToken token) { return token instanceof JwtToken; } //权限校验 @Override//传过来的principals就是认证时候传的 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { System.out.println("授权~~~~~"); AccountProfile profile = (AccountProfile) principals.getPrimaryPrincipal(); String username = profile.getUsername();//拿到认证中所使用的principals来获取信息 List roles = permissionService.getRoles(username); List permissions; if(roles.get(0).containsValue("管理员")){//默认每一个人至少有一个角色 permissions = permissionService.getAllPermission(); }else { permissions = permissionService.getPermission(username); } SimpleAuthorizationInfo info=new SimpleAuthorizationInfo(); //查询数据库来获取用户的角色 for(JSONObject role: roles){ info.addRole(role.getString("role_name")); } //查询数据库来获取用户的权限 for(JSONObject permission: permissions){ info.addStringPermission(permission.getString("permission_code")); } return info; } //登录认证校验 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String jwtToken = (String) token.getPrincipal(); String username = null; try { username = jwtUtils.getUsername(jwtToken); }catch (Exception e){ throw new AuthenticationException("token非法,不是规范的token,可能被篡改了,或者过期了"); } if (!jwtUtils.verify(jwtToken)||username==null){ throw new AuthenticationException("token认证失效,token错误或者过期,重新登陆"); } //获取用户内容,判断用户是否存在### JSONObject user = userService.findUserByName(username); if(user == null){ throw new UnknownAccountException("账户不存在"); } if(user.getString("status").equals(-1)){ throw new LockedAccountException("账户被锁定"); } AccountProfile profile = new AccountProfile(user.getLong("id") ,user.getString("username"),user.getString("email")); //BeanUtil.copyProperties(user,profile);//将user数据转移到profile //原先这里SimpleAuthenticationInfo构造的时候传入的是username, // 而redis做缓存是需要key,value的,这里必须要传入user,获取id做key. //相应的授权方法中获取身份信息也要获取user return new SimpleAuthenticationInfo(profile,jwtToken,getName()); //加盐处理,这个地方使用redis因为ByteSource没有实现Serializable接口 //需要自己定义重写定义序列化 //return new SimpleAuthenticationInfo(profile, ByteSource.Util.bytes("盐值"),getName()); } }ShiroConfig:shiro过滤器的配置 package com.xiaoke.config.shiro; import com.xiaoke.config.shiro.AccountRealm; import com.xiaoke.config.shiro.JwtFilter; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.mgt.DefaultSessionStorageEvaluator; import org.apache.shiro.mgt.DefaultSubjectDAO; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.realm.Realm; import org.apache.shiro.session.mgt.SessionManager; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition; import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; import org.crazycake.shiro.RedisCacheManager; import org.crazycake.shiro.RedisSessionDAO; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import javax.annotation.Resource; import javax.servlet.Filter; @Configuration public class ShiroConfig { @Autowired private JwtFilter jwtFilter; @Bean @Resource public SessionManager sessionManager(RedisSessionDAO redisSessionDAO) { DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); sessionManager.setSessionDAO(redisSessionDAO); return sessionManager; } @Bean @Resource public DefaultWebSecurityManager securityManager(Realm realm, SessionManager sessionManager, RedisCacheManager redisCacheManager) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(realm); redisCacheManager.setPrincipalIdFieldName("id"); securityManager.setSessionManager(sessionManager); securityManager.setCacheManager(redisCacheManager); //关闭shiro自带的session,之后采用token的形式来保存数据 DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO(); DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator(); defaultSessionStorageEvaluator.setSessionStorageEnabled(false); subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator); securityManager.setSubjectDAO(subjectDAO); return securityManager; } @Bean public ShiroFilterChainDefinition shiroFilterChainDefinition() { DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition(); Map filterMap = new LinkedHashMap(); filterMap.put("/**", "jwt"); // 主要通过注解方式校验权限 chainDefinition.addPathDefinitions(filterMap); return chainDefinition; } @Bean("shiroFilterFactoryBean") public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager, ShiroFilterChainDefinition shiroFilterChainDefinition) { ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean(); shiroFilter.setSecurityManager(securityManager); Map filters = new HashMap(); filters.put("jwt", jwtFilter);//配置自定义过滤器 shiroFilter.setFilters(filters); Map filterMap = shiroFilterChainDefinition.getFilterChainMap();//配置拦截请求 //设置后所有请求通过jwt认证 shiroFilter.setFilterChainDefinitionMap(filterMap); return shiroFilter; } @Bean(name = "realm") public Realm getRealm(){ AccountRealm accountRealm = new AccountRealm(); accountRealm.setCachingEnabled(true); //启用身份验证缓存,即缓存AuthenticationInfo信息,默认false accountRealm.setAuthenticationCachingEnabled(true); //缓存AuthenticationInfo信息的缓存名称 在ehcache-shiro.xml中有对应缓存的配置 accountRealm.setAuthenticationCacheName("authenticationCache"); //启用授权缓存,即缓存AuthorizationInfo信息,默认false accountRealm.setAuthorizationCachingEnabled(true); //缓存AuthorizationInfo信息的缓存名称 在ehcache-shiro.xml中有对应缓存的配置 accountRealm.setAuthorizationCacheName("authorizationCache"); /* //修改凭证校验匹配器 HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); //设置加密算法为MD5 hashedCredentialsMatcher.setHashAlgorithmName("MD5"); //设置散列次数 hashedCredentialsMatcher.setHashIterations(1024); accountRealm.setCredentialsMatcher(hashedCredentialsMatcher); */ return accountRealm; } }UserController:user控制器(可自行编写,只做参考) package com.xiaoke.controller; import com.alibaba.fastjson.JSONObject; import com.xiaoke.entity.User; import com.xiaoke.mapper.PermissionMapper; import com.xiaoke.service.UserService; import com.xiaoke.util.contants.ErrorEnum; import com.xiaoke.util.model.JwtUtils; import com.xiaoke.util.model.Result; import org.apache.shiro.authz.annotation.RequiresAuthentication; import org.apache.shiro.authz.annotation.RequiresRoles; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.UnsupportedEncodingException; import java.util.List; /** ** 前端控制器 * * * @author anonymous * @since 2022-04-18 */ @RestController @RequestMapping("/user") public class UserController { @Resource private UserService userService; @Resource private PermissionMapper permissionMapper; @RequiresAuthentication @RequiresRoles("管理员") @GetMapping("/index") public JSONObject index(){ return userService.getById(1); } @GetMapping("/Login") public JSONObject login(User user, HttpServletResponse response) throws UnsupportedEncodingException { JSONObject userDb = userService.findUserByName(user.getUsername()); if(userDb!=null){ if(userDb.getString("password").equals(user.getPassword())){ String token = JwtUtils.createToken(userDb); response.setHeader("Authorization",token); return Result.successLogin(); } return Result.errorJson(ErrorEnum.E_password); } return Result.errorJson(ErrorEnum.E_username);//Result为返回状态信息,可自行编写 } @RequiresAuthentication @GetMapping("/getUser") public JSONObject getUser(HttpServletRequest request,HttpServletResponse response){ JSONObject data = new JSONObject(); String token = request.getHeader("Authorization"); String username = JwtUtils.getUsername(token); List roles = permissionMapper.getRoles(username); List permission; data.put("roles",roles); if(roles.get(0).containsValue("管理员")){ permission = permissionMapper.getAllPermission(); }else { permission = permissionMapper.getPermission(username); } data.put("permission",permission); response.setHeader("Authorization",token); return Result.successJson(data); } @GetMapping("/register") public JSONObject register(User user){ return userService.register(user); } }GlobalExceptionHandler:全局异常处理(可自行编写,只做参考) package com.xiaoke.common.exception; import com.alibaba.fastjson.JSONObject; import com.auth0.jwt.exceptions.TokenExpiredException; import com.xiaoke.util.contants.ErrorEnum; import com.xiaoke.util.model.Result; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.ShiroException; import org.apache.shiro.authz.UnauthenticatedException; import org.apache.shiro.authz.UnauthorizedException; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; import java.io.IOException; /** * 日志输出 * 全局捕获异常 */ @Slf4j @RestControllerAdvice public class GlobalExceptionHandler { @ResponseStatus(HttpStatus.UNAUTHORIZED) //因为前后端分离 返回一个状态 一般是401 没有权限 @ExceptionHandler(value = ShiroException.class)//捕获运行时异常ShiroException是大部分异常的父类 public JSONObject handler(ShiroException e){ log.error("运行时异常:-----------------{}",e); return Result.errorJson(e.getMessage()); } @ResponseStatus(HttpStatus.BAD_REQUEST) //因为前后端分离 返回一个状态 @ExceptionHandler(value = RuntimeException.class)//捕获运行时异常 public JSONObject handler(RuntimeException e){ log.error("运行时异常:-----------------{}",e); return Result.errorJson(e.getMessage()); } // 捕捉shiro的异常 @ResponseStatus(HttpStatus.UNAUTHORIZED) @ExceptionHandler(UnauthenticatedException.class) public JSONObject handle401(UnauthenticatedException e) { return Result.errorJson(ErrorEnum.E_20011); } @ResponseStatus(HttpStatus.UNAUTHORIZED) @ExceptionHandler(UnauthorizedException.class) public JSONObject handle(UnauthorizedException e) { return Result.errorJson(ErrorEnum.E_无权限); } }以下可自行编写,仅作参考:(可以自行编写) Result:返回信息进行封装 package com.xiaoke.util.model; import com.alibaba.fastjson.JSONObject; import com.xiaoke.util.contants.Contants; import com.xiaoke.util.contants.ErrorEnum; public class Result { public static JSONObject successJson(Object data){ JSONObject resultJson = new JSONObject(); resultJson.put("code", Contants.SUCCESS_CODE); resultJson.put("msg",Contants.SUCCESS_MSG); resultJson.put("data",data); return resultJson; } public static JSONObject successLogin(){ JSONObject resultJson = new JSONObject(); resultJson.put("code", Contants.SUCCESS_CODE); resultJson.put("msg","登录成功"); resultJson.put("data", new JSONObject()); return resultJson; } public static JSONObject errorJson(ErrorEnum errorEnum){ JSONObject resultJson = new JSONObject(); resultJson.put("code", errorEnum.getErrorCode()); resultJson.put("msg", errorEnum.getErrorMsg()); resultJson.put("data", new JSONObject()); return resultJson; } public static JSONObject errorJson(String errorEnum){ JSONObject resultJson = new JSONObject(); resultJson.put("code", 401); resultJson.put("msg", errorEnum); resultJson.put("data", new JSONObject()); return resultJson; } } package com.xiaoke.util.contants; //通用常量方便管理 public class Contants { public static final String SUCCESS_CODE = "200"; public static final String SUCCESS_MSG = "请求成功"; } package com.xiaoke.util.contants; /** */ public enum ErrorEnum { /* * 错误信息 * */ E_10010("10010","登录失败,请重新登录"), E_20011("401", "登陆已过期,请重新登陆"), E_username("400","用户名错误"), E_password("400","密码错误"), private String errorCode; private String errorMsg; ErrorEnum(String errorCode, String errorMsg) { this.errorCode = errorCode; this.errorMsg = errorMsg; } public String getErrorCode() { return errorCode; } public String getErrorMsg() { return errorMsg; } }如果shiro中要进行密码加盐处理:开启ShiroConfig中校验匹配器,在redis序列化会出现问题可以看以下进行处理: (75条消息) springBoot+shiro+redis缓存实现时,反序列化的一个错误no valid constructor_Eden4J的博客-CSDN博客 在运行时可能出现的一些问题: 1.redis服务没有开启 2.application.yml配置中数据库信息错误 3.相关依赖没有添加完成 以上是本人花费一些时间查找相关资料以及debug之后的总结,如果有更好的方法或者建议,欢迎互相学习 |
今日新闻 |
推荐新闻 |
CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3 |