springboot-shiro-jwt-redis实现用户登录的认证与授权(前后端分离)
shiro-jwt-redis实现用户认证、授权大致流程
认证时进行缓存获取数据,否则进入认证方法(可以自己debug弄清流程更好)
相关依赖:
主要依赖:
<dependency><!--包括shiro以及shiro-redis依赖--><groupId>org.crazycake</groupId><artifactId>shiro-redis-spring-boot-starter</artifactId><version>3.2.1</version></dependency>
<!--引⼊jwt--><dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>3.10.3</version></dependency>
代码中使用的工具依赖:
<!--fastjson--><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.35</version></dependency>
避免启动失败:
在META-INF下创建spring-devtoos.propertis配置文件在里面编写
restart.include.shiro-redis=/shiro-[\\w-\\.]+jar
application.yml配置
# DataSource Configspring:datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/vueblog?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghaiusername: rootpassword: 123456mybatis-plus:mapper-locations: classpath*:/mapper/**Mapper.xmlserver:port: 8081logging: #sql日志level:com.xiaoke.mapper: DEBUG #dao接口全限定名称shiro-redis:enabled: trueredis-manager:host: 127.0.0.1:6379
JwtUtils :生成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.ponent;import java.io.UnsupportedEncodingException;import java.util.Date;import java.util.HashMap;import java.util.Map;// jwt工具类@Slf4j@Data //自动生成setter、getter方法,可以直接写@Componentpublic 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<String ,Object> map=new HashMap<>();map.put("alg","HS256");map.put("typ","JWT");//使用jwt的api生成tokenString 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;}@Overridepublic Object getPrincipal() {return token;}@Overridepublic 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.ponent;import javax.servlet.ServletRequest;import javax.servlet.ServletResponse;import javax.servlet.http.HttpServletRequest;@Componentpublic 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中loginreturn true;}catch (Exception e){return false;}}}}
AccountProfile:用来处理redis必须获取id做key
package com.xiaoke.config.shiro;import lombok.Data;import java.io.Serializable;@Datapublic 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.ponent;import javax.annotation.Resource;import java.util.List;@Componentpublic class AccountRealm extends AuthorizingRealm {@ResourceJwtUtils jwtUtils;@ResourceUserService userService;@ResourcePermissionService permissionService;//为了让realm支持jwt的凭证校验@Overridepublic 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<JSONObject> roles = permissionService.getRoles(username);List<JSONObject> 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;}//登录认证校验@Overrideprotected 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.//相应的授权方法中获取身份信息也要获取userreturn 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;@Configurationpublic class ShiroConfig {@Autowiredprivate JwtFilter jwtFilter;@Bean@Resourcepublic SessionManager sessionManager(RedisSessionDAO redisSessionDAO) {DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();sessionManager.setSessionDAO(redisSessionDAO);return sessionManager;}@Bean@Resourcepublic 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;}@Beanpublic ShiroFilterChainDefinition shiroFilterChainDefinition() {DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();Map<String, String> 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<String, Filter> filters = new HashMap<>();filters.put("jwt", jwtFilter);//配置自定义过滤器shiroFilter.setFilters(filters);Map<String, String> filterMap = shiroFilterChainDefinition.getFilterChainMap();//配置拦截请求//设置后所有请求通过jwt认证shiroFilter.setFilterChainDefinitionMap(filterMap);return shiroFilter;}@Bean(name = "realm")public Realm getRealm(){AccountRealm accountRealm = new AccountRealm();accountRealm.setCachingEnabled(true);//启用身份验证缓存,即缓存AuthenticationInfo信息,默认falseaccountRealm.setAuthenticationCachingEnabled(true);//缓存AuthenticationInfo信息的缓存名称 在ehcache-shiro.xml中有对应缓存的配置accountRealm.setAuthenticationCacheName("authenticationCache");//启用授权缓存,即缓存AuthorizationInfo信息,默认falseaccountRealm.setAuthorizationCachingEnabled(true);//缓存AuthorizationInfo信息的缓存名称 在ehcache-shiro.xml中有对应缓存的配置accountRealm.setAuthorizationCacheName("authorizationCache");/* //修改凭证校验匹配器HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();//设置加密算法为MD5hashedCredentialsMatcher.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;/*** <p>* 前端控制器* </p>** @author anonymous* @since -04-18*/@RestController@RequestMapping("/user")public class UserController {@Resourceprivate UserService userService;@Resourceprivate 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<JSONObject> roles = permissionMapper.getRoles(username);List<JSONObject> 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 mon.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@RestControllerAdvicepublic 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之后的总结,如果有更好的方法或者建议,欢迎互相学习
springboot-shiro-jwt-redis实现用户登录的认证与授权(前后端分离)需要有一定shiro jwt redis springboot基础
如果觉得《springboot-shiro-jwt-redis实现用户登录的认证与授权(前后端分离)需要有一定shiro》对你有帮助,请点赞、收藏,并留下你的观点哦!