springboot-shiro-jwt-redis实现用户登录的认证与授权(前后端分离)需要有一定shiro、jwt、redis、springboot基础

1 篇文章 0 订阅
1 篇文章 0 订阅

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 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: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.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<String ,Object> 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<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;
    }
    //登录认证校验
    @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<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信息,默认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;
​
/**
 * <p>
 *  前端控制器
 * </p>
 *
 * @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<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 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之后的总结,如果有更好的方法或者建议,欢迎互相学习

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值