Shiro框架-shiro jwt redis实现登录功能

注:使用前后端分离.

1.导入需要的springboot的pom依赖文件

<dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <version>8.0.18version>
        dependency>
        <dependency>
            <groupId>com.mchangegroupId>
            <artifactId>c3p0artifactId>
            <version>0.9.5.4version>
        dependency>
        <dependency>
            <groupId>javax.servletgroupId>
            <artifactId>servlet-apiartifactId>
            <version>3.0-alpha-1version>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-configuration-processorartifactId>
        dependency>
        <dependency>
            <groupId>io.jsonwebtokengroupId>
            <artifactId>jjwtartifactId>
            <version>0.9.1version>
        dependency>
        <dependency>
            <groupId>org.apache.shirogroupId>
            <artifactId>shiro-springartifactId>
            <version>1.4.0version>
        dependency>
        <dependency>
            <groupId>redis.clientsgroupId>
            <artifactId>jedisartifactId>
            <version>3.1.0version>
        dependency>

1.工具类的创建

我在保存密码中使用的md5加密所以需要创建工具类

MD5Util工具类:

    private static final String SALT = "123456789";
    //生成md5加密后的密码
	public static String encode(String password){
        String s = addSalt(password, SALT);
        return DigestUtils.md5DigestAsHex(s.getBytes());
    }
	//将salt于密码进行拼接,让密码更加复杂
    private static String addSalt(String password,String salt){
        return salt.charAt(0)+password+salt;
    }

创建jedispool:

# Redis服务器地址
redis.host=127.0.0.1
# Redis服务器连接端口
redis.port=6379
# Redis服务器连接密码(默认为空)
redis.password=
# 连接超时时间(毫秒)
redis.timeout=10000
# 连接池最大连接数(使用负值表示没有限制)
redis.pool.maxActive=200
# 连接池最大阻塞等待时间(使用负值表示没有限制)
redis.pool.maxWait=-1
# 连接池中的最大空闲连接
redis.pool.minIdle=8
# 连接池中的最小空闲连接
redis.pool.maxIdle=0
@Configuration
@PropertySource("classpath:jedisConfig.properties")
public class JedisConfig {
    @Value("${redis.host}")
    private  String host;
    @Value("${redis.port}")
    private Integer port;
    @Value("${redis.password}")
    private  String password ;
    @Value("${redis.pool.maxIdle}")
    private int maxIdle;
    @Value("${redis.pool.maxActive}")
    private int maxActive;
    @Value("${redis.pool.maxWait}")
    private int maxWait;
    @Value("${redis.pool.minIdle}")
    private int minIdle;
    @Value("${redis.timeout}")
    private int timeout ;
    @Bean
    public JedisPool pool(){
        try {
            JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
            jedisPoolConfig.setMaxIdle(maxIdle);
            jedisPoolConfig.setMaxWaitMillis(maxWait);
            jedisPoolConfig.setMaxTotal(maxActive);
            jedisPoolConfig.setMinIdle(minIdle);
            // 密码为空设置为null
            if (StringUtil.isEmpty(password)) {
                password = null;
            }
            JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout, password);
            if (jedisPool==null){
                System.out.println("初始化Redis连接池JedisPool失败!地址: " + host + ":" + port);
            }
            System.out.println("初始化Redis连接池JedisPool成功!地址: " + host + ":" + port);
            return jedisPool;
        } catch (Exception e) {
            System.out.println("初始化Redis连接池JedisPool异常:" + e.getMessage());
        }
        return null;
    }
}

创建jedisUtil用于redis的连接

@Component
public class JedisUtil {

    private static JedisPool jedisPool;

    @Autowired
    public void setJedisPool(JedisPool jedisPool) {
        JedisUtil.jedisPool = jedisPool;
    }
	//添加
    public static boolean set(String key ,String value,Integer timeout){
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            String s = jedis.set(key, value);
            if (timeout!=null){
                jedis.expire(key,timeout);
            }
            if (s.equals("ok")){
                return false;
            }
            return true;
        }catch(Exception e){
            e.printStackTrace();
            return false;
        }finally{
            jedis.close();
        }
    }
    //删除
    public static boolean del(String key ){
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            jedis.del(key);
            return true;
        }catch(Exception e){
            e.printStackTrace();
            return false;
        }finally{
            jedis.close();
        }
    }
    //判断是否存在
    public static boolean isExist(String key ){
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            return jedis.exists(key);
        }catch(Exception e){
            e.printStackTrace();
            return false;
        }finally{
            jedis.close();
        }
    }
    //获取信息
    public static String get(String key){
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            return jedis.get(key);
        }catch(Exception e){
            e.printStackTrace();
            return null;
        }finally{
            jedis.close();
        }
    }
    //获取超时时间
    public static long getTimeOut(String key){
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            return jedis.ttl(key);
        }catch(Exception e){
            e.printStackTrace();
            return -1;
        }finally{
            jedis.close();
        }
    }
}

创建生成jwt的工具类:

public class JwtTokenUtil {
    //秘钥..可以自己定义.需要复杂,简单会报错
    private static final String SECRET= "11111111111";
    //过期时间 单位毫秒
    private static final long EXPSECOND = 60*1000*24*60*7;
    //刷新令牌时间 单位秒
    private static final int REFRESHSECOND = 60*60*24*7;
    private JwtTokenUtil(){}
	
    //生成jwt
    public static String encode(String username){
        long start = System.currentTimeMillis();
        JwtBuilder jwtBuilder = Jwts.builder().setHeaderParam("typ", "JWT")
                .claim("username",username)
                .setExpiration(new Date(start + EXPSECOND))
                .signWith(SignatureAlgorithm.HS256, SECRET);
        return jwtBuilder.compact();
    }
    //jwt过期后重新生成jwt
    public static String refreshToken(String jwt){
        Claims claims = decode(jwt);
        String username = (String)claims.get("username");
        return encode(username);
    }
    //解码jwt
    public static Claims decode(String jwt){
        Claims claims = null;
        try {
            claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(jwt).getBody();
        }catch(ExpiredJwtException e){
            e.printStackTrace();
            claims = e.getClaims();
        }finally{
            return claims;
        }

    }
    //获取令牌在redis中过期时间
    public static int getRefreshSecond() {
        return REFRESHSECOND;
    }
	//前端发送的token可能有Bearer前缀,解析前需要删除
    public static String delBearer(String token){
        return token.replace("Bearer","").trim();
    }
    //添加Bearer前缀
    public static String addBearer(String token){
        return "Bearer "+token;
    }
    //判断是否过期,过期解析会抛出异常,返回true
    public static boolean isExpired(String token){
        try {
            Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
            return false;
        }catch(ExpiredJwtException e){
           return true;
        }
    }

}

2.Token

public class JwtToken implements AuthenticationToken {
    private String token;
    @Override
    public Object getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return token;
    }
    public JwtToken(String string){
        this.token = string;
    }
}

3.Realm

用于身份的验证

public class UserRealm implements Realm {
    @Override
    public String getName() {
        return "userRealm";
    }
	
    //判断token是否实现authenticationToken,必须,不然报错
    @Override
    public boolean supports(AuthenticationToken authenticationToken) {
        return authenticationToken instanceof JwtToken;
    }
	
    //验证用户身份
    @Override
    public AuthenticationInfo getAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String token = (String) authenticationToken.getCredentials();
        if (JwtTokenUtil.isExpired(JwtTokenUtil.delBearer(token))){
            throw new ExpiredJwtException(null,null,null,null);
        }
        Claims claim = JwtTokenUtil.decode(JwtTokenUtil.delBearer(token));
        String username = (String)claim.get("username");
        if (JedisUtil.get(Constant.TOKEN+username).equals(token)){
            return new SimpleAuthenticationInfo(token,token,getName());
        }
        throw new AuthenticationException("失败");
    }
}

3.Filter

public class JwtFilter extends BasicHttpAuthenticationFilter {
    
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        //获取请求头中的token
        String header = getAuthzHeader(request);
        JwtToken token = new JwtToken(header);
        //提交到realm验证
        this.getSubject(request,response).login(token);
        //报错不会返回true
        return true;
    }
    //判断是否登录,通过是否携带token
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        String token = getAuthzHeader(request);
        return !StringUtil.isEmpty(token);
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        this.sendChallenge(request, response);
        return false;
    }
	//支持跨域
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
        HttpServletResponse httpServletResponse = WebUtils.toHttp(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"));
        // 跨域时会首先发送一个OPTIONS请求,这里我们给OPTIONS请求直接返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        // 查看当前Header中是否携带Authorization属性(Token),有的话就进行登录认证授权
        if (this.isLoginAttempt(request, response)) {
            try {
                // 进行Shiro的登录UserRealm
                this.executeLogin(request, response);
                return true;
            } catch (Exception e) {
                return false;
            }
        }else{
            this.response401(response,"未携带Token");
            return false;
        }
    }
    /**
     * 无需转发,直接返回Response信息
     */
    private void response401(ServletResponse response, String msg){
        HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
        httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setContentType("application/json; charset=utf-8");
        try (PrintWriter out = httpServletResponse.getWriter()) {
            out.append(msg);
        } catch (IOException e) {
            throw new BaseException("直接返回Response信息出现IOException异常:" + e.getMessage());
        }
    }
}

4.shiroConfig

注:UserRealm不能通过注入方式生成,必须通过new,不然filterChainDefinitionMap的顺序将失效

造成无法拦截的情况

public class ShiroConfig {
    /**
     * 配置使用自定义Realm,关闭Shiro自带的session
     */
    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
    @Bean("securityManager")
    public DefaultWebSecurityManager defaultWebSecurityManager() {
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        // 使用自定义Realm
        defaultWebSecurityManager.setRealm(new UserRealm());
        // 关闭Shiro自带的session
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        defaultWebSecurityManager.setSubjectDAO(subjectDAO);

        return defaultWebSecurityManager;
    }

    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        // 添加自己的过滤器取名为jwt
        Map<String, Filter> filterMap = new HashMap<>(16);
        filterMap.put("jwt", new JwtFilter());
        factoryBean.setFilters(filterMap);
        factoryBean.setSecurityManager(securityManager);
        // 自定义url规则使用LinkedHashMap有序Map
        LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>(16);
		//anon为匿名访问
        filterChainDefinitionMap.put("/anon/**", "anon");
        filterChainDefinitionMap.put("/user/**", "jwt");

        factoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return factoryBean;
    }

    /**
     * 下面的代码是添加注解支持
     */
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
}

5.Interceptor

有些请求需要使用用户数据,我们将从token中获取用户数据,解析后保存到request中,方便使用

public class tokenHeaderInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String header = request.getHeader("Authorization");
        String token = JwtTokenUtil.delBearer(header);
        Claims claim = JwtTokenUtil.decode(token);
        String username = (String)claim.get("username");
        if (!StringUtil.isEmpty(username)){
            request.setAttribute("username",username);
        }
        response.setContentType("application/json;charset=utf-8");
        request.setCharacterEncoding("UTF-8");
        response.setCharacterEncoding("UTF-8");
        return true;
        //包含Beare开头
    }
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        response.setCharacterEncoding("UTF-8");
    }
}

对需要使用用户信息的路径将其添加到配置文件中

public class WebConfig extends WebMvcConfigurationSupport {
    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        LinkedList<String> list = new LinkedList<>();
        list.add("/user/login");
        list.add("/user/register");
        System.out.println("llllllllllllllll");
        registry.addInterceptor(new tokenHeaderInterceptor()).addPathPatterns("/user/**").excludePathPatterns(list);
    }
    //跨域
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
                .allowCredentials(false).maxAge(3600);
    }
}

6.LoginController

@RestController
@RequestMapping("/anon")
public class CommonController {
    @Autowired
    private UserService userService;
    @Autowired
    private JedisUtil jedisUtil;

    @RequestMapping("/login")
    public Result login(@RequestBody Map map,HttpServletRequest req){
        //通过Interceptor后,username将储存在req中
        String username = (String)map.get("username");
        //加密密码
        String password = MD5Util.encode(((String)map.get("password")).trim());
        //通过username查询数据库密码
        User user = userService.selectUserByUsername(username);
        if (user ==null){
            return new Result(HttpStatus.EXPECTATION_FAILED.value(),"用户不存在",null);
        }
        if (user.getPassword().equals(password)){
            String token = JwtTokenUtil.addBearer((JwtTokenUtil.encode(username)));
            jedisUtil.set(Constant.TOKEN+username,token, JwtTokenUtil.getRefreshSecond());
           
            return new Result(HttpStatus.OK.value(),"登录成功",token);
        }
        return new Result(HttpStatus.UNAUTHORIZED.value(),"未找到用户信息",null);
    }
}

7.验证

我们将使用postman进行验证

1.登录

Shiro框架-shiro jwt redis实现登录功能_第1张图片

返回结果:

Shiro框架-shiro jwt redis实现登录功能_第2张图片

在data中返回了jwt的字符串,表示成功.

2.不携带token访问需要登录的路径

Shiro框架-shiro jwt redis实现登录功能_第3张图片

可以看见返回未携带token的信息.

3.携带token访问需要登录的路径

添加token

结果:

Shiro框架-shiro jwt redis实现登录功能_第4张图片

你可能感兴趣的:(shiro框架)