springboot+redis+shiro+jwt实现权限管理

文章目录

  • 一、设计思路
    • 1、用户登录
    • 2、Token验证
    • 3、资源访问
  • 二、 环境搭建
    • 1、pom依赖
    • 2、mybatis&redis
  • 二、shiro配置
    • 1、CustomRealm
    • 2、JWTFilter(不建议使用注解由spring工厂创建对象,不然设置公共资源无效)
    • 3、ShiroConfig
    • 4、MD5Util
  • 三、redis配置
    • 1、自定义RedisTemplate
    • 2、RedisUtil
  • 四、JWT配置
    • 1、JWTToken
    • 2、TokenUtils
  • 五、统一日志打印和异常处理
    • 1、MyLog
    • 2、MyExceptionHandler
  • 六、Vo
  • 七、签发token
  • 总结


一、设计思路

1、用户登录

前端用户未登录仅可访问登录页面和主界面,后端过滤器放行图片,jar,登录注册等接口,通过login方法验证用户,返回登录成功后的token信息并且存储在redis中。前端每次发起的请求都需要在http头部携带上token用来验证身份,并且设置统一的返回结果处理token失效以及权限不足

2、Token验证

重写BasicHttpAuthenticationFilter,重写PreHandle方法(跨域设置,因为http头部携带了authorization所以会发起两次请求,应当直接放行第一次options方法请求,验证token是否为空),执行super.executeLogin方法(获取createToken中的自定义token,并且执行subject.login(token)方法,调用自定义realm的doAuthentication方法),如果验证成功,那么执行重写的onLoginSuccess(验证token是否过期),如果验证事变,执行重写的onLoginFailure(处理登录的异常)。最后自定义一个方法返回一个vo类。
springboot+redis+shiro+jwt实现权限管理_第1张图片

3、资源访问

springboot+redis+shiro+jwt实现权限管理_第2张图片

二、 环境搭建

1、pom依赖

<dependency>
			<groupId>org.springframework.bootgroupId>
			<artifactId>spring-boot-starter-webartifactId>
		dependency>

		<dependency>
		    <groupId>org.springframework.bootgroupId>
		    <artifactId>spring-boot-starter-aopartifactId>
		dependency>

		<dependency>
	        <groupId>org.projectlombokgroupId>
	        <artifactId>lombokartifactId>
	        <optional>trueoptional>
	    dependency>

	    <dependency>
	        <groupId>com.baomidougroupId>
	        <artifactId>mybatis-plus-boot-starterartifactId>
	        <version>3.3.1.tmpversion>
	    dependency>

	    <dependency> 
	    	<groupId>mysqlgroupId> 
	    	<artifactId>mysql-connector-javaartifactId> 
	    	<version>8.0.13version>
	    dependency>

		<dependency>
		    <groupId>com.baomidougroupId>
		    <artifactId>mybatis-plus-generatorartifactId>
		    <version>3.3.1.tmpversion>
		dependency>
		<dependency>
		    <groupId>org.apache.velocitygroupId>
		    <artifactId>velocity-engine-coreartifactId>
		    <version>2.2version>
		dependency>



		<dependency>
		    <groupId>com.alibabagroupId>
		    <artifactId>fastjsonartifactId>
		    <version>1.2.73version>
		dependency>


		

		<dependency>
			<groupId>com.auth0groupId>
			<artifactId>java-jwtartifactId>
			<version>3.8.2version>
		dependency>


		<dependency>
			<groupId>org.springframework.bootgroupId>
			<artifactId>spring-boot-starter-data-redisartifactId>
		dependency>

		<dependency>
			<groupId>org.apache.shirogroupId>
			<artifactId>shiro-springartifactId>
			<version>1.4.0version>
		dependency>

2、mybatis&redis

#mysql8的配置
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/wlw?useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=


spring.redis.port=6379
spring.redis.host=localhost

二、shiro配置

1、CustomRealm

public class CustomRealm extends AuthorizingRealm {


    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("shiro授权");
        //获取用户名
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        String username = TokenUtil.getUser(PrincipalCollection.getPrincipal().toString())
        //根据用户名从自己的数据库中获取role和permission信息
        return info;
    }

    /**
     * 登录验证
     * 因为token是jwt签名的,所以可以利用jwt的verify方法验证,而不需要查找数据库
     * @param authenticationToken token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("shiro验证");
        String principal = authenticationToken.getPrincipal().toString();//获取token
        String userName = TokenUtil.getUser(principal);
        User user = null;
        //从自己的数据判断查找user
        if(user!=null){
            SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal,principal,getName());
            return info;
        }
        return null;
    }

    /**
     * 将自定义的realm需要支持自定义的token,必须重写方法,不然运行时会出错。
     * @param token
     * @return
     */
    @Override
    public boolean supports(AuthenticationToken token){
        return token != null && token instanceof JWTToken;
    }
}

2、JWTFilter(不建议使用注解由spring工厂创建对象,不然设置公共资源无效)

public class JWTFilter extends BasicHttpAuthenticationFilter {

    private  RedisUtil redisUtil;

    public RedisUtil getRedisUtil() {
        return redisUtil;
    }

    public void setRedisUtil(RedisUtil redisUtil) {
        this.redisUtil = redisUtil;
    }

    /**
     * 判断是否允许通过
     * @param request
     * @param response
     * @param mappedValue
     * @return
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        System.out.println("执行登录方法");
        try{
            return executeLogin(request,response);
        }catch (Exception e){
            //登录时失败,也就是用户不存在
            //responseError(response,"token出现异常",445);
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 是否进行登录请求,判断authorization是否为空,也就是是否携带token信息
     * @param request
     * @param response
     * @return
     */
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        //String token=((HttpServletRequest)request).getHeader("Authorization");
        HttpServletRequest servletRequest = (HttpServletRequest) request;
        String token = servletRequest.getHeader("Authorization");
       /* Enumeration headers = ((HttpServletRequest) request).getHeaderNames();
        while(headers.hasMoreElements()){
            String header = headers.nextElement();
            System.out.println(header+":"+servletRequest.getHeader(header));
        }*/
        //System.out.println(token);
        if (StringUtils.isAnyBlank(token)){
            return false;
        }
        return true;
    }

    /**
     * 创建shiro token
     * @param request
     * @param response
     * @return
     */
    @Override
    protected JWTToken createToken(ServletRequest request, ServletResponse response) {
        System.out.println("创建token");
        //String jwtToken = ((HttpServletRequest)request).getHeader("token");
        String jwtToken = ((HttpServletRequest)request).getHeader("Authorization");
        if(jwtToken!=null) return new JWTToken(jwtToken);
        return null;
    }
    /**
     * isAccessAllowed为false时调用,验证失败
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
       /* System.out.println("token登录失败");
        this.sendChallenge(request,response);
        responseError(response,"token登录失败",445);*/
        return false;
    }



    /**
     * shiro验证成功调用
     * 已经执行了登入方法,token验证成功
     * @param token
     * @param subject
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
        //System.out.println("token登录成功,验证过期时间");
        String jwttoken= (String) token.getPrincipal();
        //这里已经执行
        //这里的token经过executeLogin的验证,肯定是不为空的
        //检验token是否正确
        long expireTime = redisUtil.getExpire(TokenUtil.getUser(jwttoken));
        //System.out.println(expireTime);
        if(expireTime>0) {
            //System.out.println("验证成功,允许访问");
            return true;//验证成功
        }
        responseError(response,"token失效",444);
        return false;//验证失
    }

    /**
     * 拦截器的前置方法,此处进行跨域处理
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @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"));
        httpServletResponse.setHeader("Access-Control-Allow-Credentials","true");
        //解决前端发起两次请求,第一次请求方式是预请求,不会携带token,
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())){
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;//预请求不做处理
        }
        //System.out.println(httpServletRequest.getHeader("Authorization"));

        //如果不带token,不去验证shiro
        if (!isLoginAttempt(request,response)){
            responseError(response,"token不存在,未登录",446);//未登录
            return false;
        }
        //System.out.println("token存在");
        return super.preHandle(request,response);
    }



    /**
     * 登录操作时,出现异常
     * @param token
     * @param e
     * @param request
     * @param response
     * @return
     */
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        //登录时出现异常
        System.out.println("登录时出现异常");
        if(e instanceof UnknownAccountException) responseError(response,"token不正确",445);
        return false;
    }

    /**
     * 验证失败
     * @param response
     * @param msg
     */
    private void responseError(ServletResponse response,String msg,Integer code){
        //System.out.println("返回错误信息");
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        httpResponse.setStatus(200);
        httpResponse.setCharacterEncoding("UTF-8");
        Result result = new Result(msg).NO(code);
        String jsonStr = JSON.toJSONString(result);//需要标注get方法
        //System.out.println(result);
        httpResponse.setContentType("application/json;charset=UTF-8");
        try {
            httpResponse.getWriter().print(jsonStr);
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("返回错误失败");
        }
    }
}

3、ShiroConfig

@Configuration
public class ShiroConfig {
    /**
     * 这里的JWTFilter不能由spring创建,所有资源都会被拦截,必须在创建工厂对象中创建对象
     * @param securityManager 安全管理器
     * @param redisUtil redis工具类
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,RedisUtil redisUtil){
        ShiroFilterFactoryBean shiroFilterFactoryBean=new ShiroFilterFactoryBean();
        //设置自己的过滤器
        Map<String, Filter> filterMap=new LinkedHashMap();
        //filterMap.put("jwt", filter);
        JWTFilter filter = new JWTFilter();
        filter.setRedisUtil(redisUtil);
        filterMap.put("jwt", filter);
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //不要用HashMap来创建Map,会有某些配置失效,要用链表的LinkedHashmap
        Map<String,String> filterRuleMap=new LinkedHashMap<>();
        //放行接口
        filterRuleMap.put("/","anon");
        filterRuleMap.put("/**/pic/**","anon");
        filterRuleMap.put("/**/webjars/**","anon");
        filterRuleMap.put("/**/login","anon");
        filterRuleMap.put("/**/regist","anon");
        filterRuleMap.put("/**/css/**","anon");
        filterRuleMap.put("/**/images/**","anon");
        filterRuleMap.put("/**/js/**","anon");
        filterRuleMap.put("/**/lib/**","anon");
        //拦截所有接口
        filterRuleMap.put("/**","jwt");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterRuleMap);
        shiroFilterFactoryBean.setFilters(filterMap);
        return shiroFilterFactoryBean;

    }


    @Bean
    public SecurityManager securityManager(CustomRealm customRealm){
        //设置自定义Realm
        DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
        securityManager.setRealm(customRealm);
        //关闭shiro自带的session
        DefaultSubjectDAO subjectDAO=new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator=new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);

        return securityManager;
    }

    @Bean
    public CustomRealm customRealm(){
        CustomRealm customRealm = new CustomRealm();
//        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
//        credentialsMatcher.setHashIterations(MD5Util.HASH_ITERATIONS);//设置散列次数
//        credentialsMatcher.setHashAlgorithmName("MD5");//设置加密方法的名称
//        customRealm.setCredentialsMatcher(credentialsMatcher);
        return customRealm;
    }

    /**
     * 授权属性源配置
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor=new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
}

4、MD5Util

public class MD5Util {

    //散列次数
    public static final int HASH_ITERATIONS = 2;

    //字符串
    private static final char [] chars  = new String("123456789abcdefghijklmnopqrstuvwxyz!@#$^%").toCharArray();

    /**
     * 获取加密密码
     * @param password 密码
     * @param salt 盐
     * @return
     */
    public static String getSecretPassword(String password,String salt){
        return new Md5Hash(password, salt, HASH_ITERATIONS).toString();
    }

    public static String getRandomSalt(int length){
        if(length<=0){throw new RuntimeException("长度不能小于等于0");}
        StringBuilder builder = new StringBuilder();
        for(int i = 0 ; i<length;i++) builder.append(chars[new Random().nextInt(chars.length)]);
        return builder.toString();
    }
}

三、redis配置

1、自定义RedisTemplate

@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String,Object> redisTemplate(@Autowired RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        //redisTemplate.setDefaultSerializer(new StringRedisSerializer());设置所有的功能序列化方式吗?
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);//这个类中的某些类不可用
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);

        redisTemplate.setKeySerializer(stringRedisSerializer);//设置key的序列化方式
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);//设置value(string.list,set,zset)序列化方式
        redisTemplate.setHashKeySerializer(stringRedisSerializer);//设置hash的key的序列化方式string.
        redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer);//设置hash的value序列化方式string不能存储对象
        redisTemplate.afterPropertiesSet();//?????

        return redisTemplate;
    }
}

2、RedisUtil

@Component
public class RedisUtil {

    @Autowired
    private  RedisTemplate<String,Object> redisTemplate;//工具类的方法是静态,并且工具类需要由spring容器加载

    //获取过期时间
    public  long getExpire(String key){
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }

    //设置token生命周期
    public  boolean expire(String key,String value,long seconds){
        try{
            redisTemplate.opsForValue().set(key,value,seconds,TimeUnit.SECONDS);
        }catch(Exception e){
            return false;
        }
        return true;
    }

    //删除token,用户退出登录
    public  boolean deleteKey(String key){
        return redisTemplate.delete(key);
    }

}

四、JWT配置

1、JWTToken

public class JWTToken implements AuthenticationToken {

    private String principal;
    private String credentials;

    public  JWTToken(){this(null);}

    public JWTToken(String principal){
        this.principal=principal;
        this.credentials=principal;
    };

    @Override
    public Object getPrincipal() {
        return this.principal;
    }

    @Override
    public Object getCredentials() {
        return this.credentials;
    }
}

2、TokenUtils

public class TokenUtil {
    private String token;
    public static final long  EXPIRE_TIME = 5*60*1000;//默认5min;
    //private static final long  REFRESH_EXPIRE_TIME = 5*60*1000;//refreshtoken到期时间5min;
    private static final String TOKEN_SECRET = "fslk432234&*465fds8^&^@*594#";//秘钥

    /**
     * 对用户信息进行秘钥加密并且返回
     * @param user 信息
     * @param currentTime 当前时间
     * @return 加密后的字符串
     */
    public static String sign(String user,long currentTime){
        String token = JWT.create()
           .withIssuer("ly")//发行人
           .withClaim("user",user)//存放数据
           .withClaim("currentTime",currentTime)//起始时间
           .withExpiresAt(new Date(currentTime+EXPIRE_TIME))//到期时间
           .sign(Algorithm.HMAC256(TOKEN_SECRET));//加密
        return token;
    }

    /**
     * 验证token是否过期
     * @param token 加密的信息
     * @return
     */
    public static Boolean verify(String token){
        //创建token验证器
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(TOKEN_SECRET)).withIssuer("ly").build();
        DecodedJWT decodedJWT = jwtVerifier.verify(token);
        System.out.println("认证通过");
        System.out.println("user:"+decodedJWT.getClaim("user").asString());
        System.out.println("过期时间:"+decodedJWT.getExpiresAt().toString());
        return true;
    }

    /***
     * 获取token的用户名
     * @param token
     * @return
     */
    public static String getUser(String  token){
        try{
            return JWT.decode(token).getClaim("user").asString();
        }catch(Exception e){
            System.out.println(e.getMessage());//解析出错
            return "error_user15456465";
        }

    }

    /**
     * 获取过期时间
     * @param token
     * @return
     */
    public static long getExpireTime(String token){
        return JWT.decode(token).getExpiresAt().getTime();
    }

}

五、统一日志打印和异常处理

1、MyLog

@Aspect
@Component
public class SysLogAspect {

    @Autowired
    private ILogService sysLogService;

    //定义切点 @Pointcut
    //在注解的位置切入代码
    @Pointcut("@annotation(日志注解)")
    public void logPoinCut() {
    }

    //切面 配置通知
    @AfterReturning("logPoinCut()")
    public void saveSysLog(JoinPoint joinPoint) throws UnsupportedEncodingException {
        //保存日志
        Log sysLog = new Log();

        //从切面织入点处通过反射机制获取织入点处的方法
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        //获取切入点所在的方法
        Method method = signature.getMethod();

        //获取操作
        MyLog myLog = method.getAnnotation(MyLog.class);
        if (myLog != null) {
            String value = myLog.value();
            sysLog.setEvent(value);//保存获取的操作
        }
        //获取请求的类名
        //String className = joinPoint.getTarget().getClass().getName();
        //获取请求的方法名
        //String methodName = method.getName();
        //sysLog.setEvent(className + "." + methodName);
        RequestAttributes ra = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes sra = (ServletRequestAttributes) ra;
        HttpServletRequest request = sra.getRequest();
        String userName = request.getHeader("Authorization");
        userName= TokenUtil.getUser(userName);
        //将操作的时间,事件,以及操作人存入到数据库中。
    }

}

2、MyExceptionHandler

@ControllerAdvice
public class MyExceptionHandler {

  /**
     * 用户处理权限不足
     * @param ex
     * @param response
     */
    @ExceptionHandler(UnauthorizedException.class)
    public void unauthorizedException(Exception ex,HttpServletResponse response){
        System.out.println(response);
        try {
            response.setContentType("application/json,utf-8");//设置前端解析数据的类型;
            response.getWriter().print(JSON.toJSONString(new Result("权限不足").NO(447)));//权限不足
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    @ExceptionHandler(NullPointerException.class)
    public void nullPointExcpetion(Exception e){
        e.printStackTrace();
    }


    @ExceptionHandler(Exception.class)
    public void exception(Exception e, HttpServletResponse response){
        try {
            response.getWriter().print(e.getMessage());
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

}

六、Vo

public class Result {
    private long resultCode;//错误编码

    private String msg;//返回描述

    private boolean success;//是否通过

    private Object res;//返回的数据

    public Result OK(){
       this.resultCode=200;
       this.success=true;
       return this;
    }

    public Result NO(){return NO(400);}

    public Result NO(long code){
        this.success=false;
        this.resultCode=code;
        return this;
    }

    public Result(String msg,Object res){
        this.msg=msg;
        this.res=res;
    }

    public Result(String msg){this(msg,null);}

    @Override
    public String toString() {
        return "{" +
                "code=" + resultCode +
                ", msg='" + msg + '\'' +
                ", success=" + success +
                ", res=" + res +
                '}';
    }

    public long getResultCode() {
        return resultCode;
    }

    public void setResultCode(long resultCode) {
        this.resultCode = resultCode;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public boolean isSuccess() {
        return success;
    }

    public void setSuccess(boolean success) {
        this.success = success;
    }

    public Object getRes() {
        return res;
    }

    public void setRes(Object res) {
        this.res = res;
    }
}

七、签发token

@RequestMapping("/login")
	public Result login(@RequestBody() User user) {
	    String token = "";
		System.out.println("登录");
        try {
			User u = userService.getUserByName(user.getName());
			String passwordInDB = u.getPassword();
    		String salt = u.getSalt();
    		String passwordEncoded = MD5Util.getSecretPassword(user.getPassword(),salt);
            if(!passwordEncoded.equals(passwordInDB)){
            	return new Result("密码错误").NO();
			}
            else {
            	//获取token返回给前端
				long currentTime = System.currentTimeMillis();
				token = TokenUtil.sign(user.getName(),currentTime);
				System.out.println("登录成功,您的token是:"+token);
				//存储到redis缓存中
				boolean flag = redisUtil.expire(u.getName(),"userExpire",TokenUtil.EXPIRE_TIME);
				if(!flag) System.out.println("redis存储失败");
            }
		} catch (Exception e) {//获取的用户
		if(e instanceof RuntimeException){}//用户名错误
        }
		return new Result("success",token).OK();
	}

总结

前后端分离下,需要设置跨域,如果我们添加了http请求头的信息,那么http会发起两次请求,第一次是options请求用来进行预检的,后端过滤器不能进行拦截,直接返回就就可以了,不然不会发起第二次请求。

你可能感兴趣的:(java,redis,spring,boot,spring,shiro)