spring-boot集成shiro和jwt

Updated on in 程序人生 with 0 views and 0 comments

再spring boot中一次引入shiro、redis、token并将他们融合,参考了之前写好的一篇文章,对之前做的配置进行了简化shiro集成jwt - 问尤龙の时光 (wenyoulong.com)

一、集成shiro

1.1 依赖版本管理

pom文件的properties中添加版本信息

<shiro-spring-boot.version>1.13.0</shiro-spring-boot.version>

1.2 依赖添加

这里前端还没搭建,用thymeleaf做个简单的登录页

        <!--  shiro  -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-web-starter</artifactId>
            <version>${shiro-spring-boot.version}</version>
        </dependency>
        <!--    thymeleaf模板    -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

1.3 认证授权

代码里的userService和roleSerivce是用于查询用户信息和用户角色的接口,换成自己的就可以了

package cn.com.wenyl.bs.config.shiro;

import cn.com.wenyl.bs.system.entity.SysRole;
import cn.com.wenyl.bs.system.entity.SysUser;
import cn.com.wenyl.bs.system.service.ISysRoleService;
import cn.com.wenyl.bs.system.service.ISysUserService;
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 javax.annotation.Resource;
import java.util.List;

/**
 * @author Swimming Dragon
 * @description: 配置认证和授权逻辑
 * @date 2023年12月05日 9:59
 */
public class SysUserRealm extends AuthorizingRealm {
    @Resource
    private ISysUserService userService;
    @Resource
    private ISysRoleService roleService;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        String id = (String) principalCollection.getPrimaryPrincipal();
        // 获取角色信息
        List<SysRole> roleList = roleService.getUserRoleList(id);
        //添加角色权限
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        for(SysRole role:roleList){
            simpleAuthorizationInfo.addRole(role.getRoleCode());
        }
        return simpleAuthorizationInfo;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
        SysUser sysUer = userService.getByUserName(usernamePasswordToken.getUsername());
        if(sysUer == null){
            throw new AuthenticationException("账号"+usernamePasswordToken.getUsername()+"不存在");
        }
        if(String.copyValueOf(usernamePasswordToken.getPassword()).equals(sysUer.getPassword())){
            return new SimpleAuthenticationInfo(sysUer,sysUer.getPassword(),getName());
        }
        throw new AuthenticationException("密码不正确");
    }
}

1.4 shiro配置

下述代码中,我们开放了登录页面和登录接口,不需要认证就能访问,其他接口页面要认证通过才能访问

package cn.com.wenyl.bs.config;

import cn.com.wenyl.bs.config.shiro.SysUserRealm;
import org.apache.shiro.realm.Realm;
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.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.*;

/**
 * @author Swimming Dragon
 * @description: 配置shiro
 * @date 2023年12月05日 9:55
 */
@Configuration
public class ShiroConfig {
    @Bean
    public SysUserRealm sysUserRealm() {
        return new SysUserRealm();
    }


    /**
     * 权限管理,配置主要是Realm的管理认证
     * 需要使用redis存储认证信息,所以,关闭session,重写缓存管理器
     * @return 安全管理器
     */
    @Bean("securityManager")
    public DefaultWebSecurityManager securityManager(SysUserRealm sysUserRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        List<Realm> list = new ArrayList<>();
        list.add(sysUserRealm);
        securityManager.setRealms(list);
        return securityManager;
    }


    /**
     * Filter工厂,设置对应的过滤条件和跳转条件
     * @param securityManager 安全管理器
     * @return 过滤组件
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(org.apache.shiro.mgt.SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //设置安全管理器
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //设置过滤请求
        Map<String,String> filterMap = new LinkedHashMap<>();
        filterMap.put("/system/login/loginPage", "anon");
        filterMap.put("/system/login/loginByUsernameAndPassword", "anon");
        filterMap.put("/**", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
        //未登录跳转地址
        shiroFilterFactoryBean.setLoginUrl("/system/login/loginPage");
        return shiroFilterFactoryBean;
    }
}

1.5 登录接口配置

package cn.com.wenyl.bs.system.controller;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;


/**
 * @author Swimming Dragon
 * @description: 登录接口
 * @date 2023年12月05日 13:23
 */
@Slf4j
@Controller
@RequestMapping("/system/login")
@Api(tags="登录")
public class LoginController {

    @GetMapping("/loginPage")
    @ApiOperation(value="登录-跳转到登录页", notes="登录-跳转到登录页")
    public String login(){
        return "login";
    }

    @GetMapping("/indexPage")
    @ApiOperation(value="登录-跳转到首页", notes="登录-跳转到首页")
    public String index(){
        return "index";
    }

    @PostMapping("/loginByUsernameAndPassword")
    @ApiOperation(value="登录-跳转到登录页", notes="登录-跳转到登录页")
    public String login(@RequestParam("username") String username,
                        @RequestParam("password") String password) {
        //1.获取Subject
        Subject subject = SecurityUtils.getSubject();
        //2.封装用户数据
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        //3.执行登录方法
        try {
            subject.login(token);
            //登录成功
            //跳转到test.html
            return "redirect:/system/login/indexPage";
        } catch (UnknownAccountException e) {
            //e.printStackTrace();
            //登录失败:用户名不存在
            System.out.println("用户名不存在");
            return "login";
        } catch (IncorrectCredentialsException e) {
            //e.printStackTrace();
            //登录失败:密码错误
            System.out.println("密码错误");
            return "login";
        }
    }
}

1.6 页面配置

在resources的template目录下新建login.html和index.html

1.6.1 index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登陆页面</title>
</head>
<body>
<h3>成功</h3>
登录成功
</body>
</html>

1.6.2 login.html

登录页面访问了登录接口,替换成自己的登录接口地址即可

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登陆页面</title>
</head>
<body>
<h3>登录</h3>
<form method="post" action="/bs/system/login/loginByUsernameAndPassword">
    用户名:<input type="text" name="username"/><br/>
    密码:<input type="password" name="password"/><br/>
    <input type="submit" value="登录"/>
</form>
</body>
</html>

1.7 测试

启动项目后访问登录接口中的index页面,由于未登录,自己跳转到了login页面

image.png

输入用户名密码登录成功后,在访问index页面就成功了

image.png

二、shiro集成redis

shiro默认适用内存缓存,就是在用一个map把数据在内存中存储,详见org.apache.shiro.cache.MapCache,这个Cache只适用于单机环境(只有一个JVM),因此我们还是使用redis作为shiro的缓存

2.1 这是shiro-redis常量

package cn.com.wenyl.bs.config.shiro;
/**
 * @description: TODO
 * @author Swimming Dragon
 * @date 2023年12月06日 15:52
 */
public class ShiroConstant {
    /**
     * shir缓存过期时间(毫秒)
     */
    public static final long SHIRO_CACHE_EXPIRE_TIME = 43200000;
}

2.2 实现Cache接口

package cn.com.wenyl.bs.config.shiro;
import cn.com.wenyl.bs.config.jwt.JwtConstant;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.springframework.data.redis.core.RedisTemplate;

import java.util.Collection;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * @author Swimming Dragon
 * @description: shiro实现redis缓存
 * @date 2023年12月05日 16:50
 */
public class ShiroRedisCache implements Cache<String, Object>{
    private final RedisTemplate<String,Object> redisTemplate;
    private final String cachePrefix;
    public ShiroRedisCache(RedisTemplate<String,Object> redisTemplate, String cachePrefix){
        super();
        this.redisTemplate = redisTemplate;
        this.cachePrefix = cachePrefix;
    }
    @Override
    public Object get(String k) throws CacheException {
        return redisTemplate.opsForValue().get(cacheKey(k));
    }

    @Override
    public Object put(String k, Object v) throws CacheException {
        redisTemplate.opsForValue().set(k,v, ShiroConstant.SHIRO_CACHE_EXPIRE_TIME, TimeUnit.MICROSECONDS);
        return v;
    }

    @Override
    public Object remove(String k) throws CacheException {
        Object v = this.get(k);
        redisTemplate.opsForValue().getOperations().delete(k);
        return v;
    }

    @Override
    public void clear() throws CacheException {
        throw new UnsupportedOperationException("不支持清理redis操作");
    }

    @Override
    public int size() {
        throw new UnsupportedOperationException("不支持获取redis键值对的大小操作");
    }

    @Override
    public Set<String> keys() {
        throw new UnsupportedOperationException("不支持获取redis所有key操作");
    }

    @Override
    public Collection<Object> values() {
        throw new UnsupportedOperationException("不支持获取redis所有value操作");
    }

    private String cacheKey(String key){
        return cachePrefix + ":" + key;
    }
}

2.3 创建CacheManager

package cn.com.wenyl.bs.config.shiro;


import lombok.Getter;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import org.springframework.data.redis.core.RedisTemplate;

/**
 * @author Swimming Dragon
 * @description: TODO
 * @date 2023年12月05日 16:44
 */
public  class ShiroRedisCacheManager implements CacheManager{
    @Getter
    private final RedisTemplate<String,Object> redisTemplate;
    public ShiroRedisCacheManager(RedisTemplate<String,Object> redisTemplate){
        this.redisTemplate = redisTemplate;
    }
    @Override
    @SuppressWarnings({"rawtypes", "unchecked"})
    public Cache getCache(String cachePrefix) throws CacheException {
        return new ShiroRedisCache(getRedisTemplate(),cachePrefix);
    }
}

2.4 shiro配置缓存

在前面我们建立郭shiro配置类,现在在里面新增缓存部分配置

package cn.com.wenyl.bs.config;

import cn.com.wenyl.bs.config.shiro.ShiroRedisCacheManager;
import cn.com.wenyl.bs.config.shiro.SysUserRealm;
import org.apache.shiro.cache.MemoryConstrainedCacheManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;

import javax.annotation.Resource;
import java.util.*;

/**
 * @author Swimming Dragon
 * @description: 配置shiro
 * @date 2023年12月05日 9:55
 */
@Configuration
public class ShiroConfig {
    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    @Bean
    public SysUserRealm sysUserRealm() {
        return new SysUserRealm();
    }

    /**
     * 权限管理,配置主要是Realm的管理认证
     * 需要使用redis存储认证信息,所以,关闭session,重写缓存管理器
     * @param sysUserRealm 用户认证管理
     * @return 安全管理器
     */
    @Bean("securityManager")
    public DefaultWebSecurityManager securityManager(SysUserRealm sysUserRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        List<Realm> list = new ArrayList<>();
        list.add(sysUserRealm);
        securityManager.setRealms(list);
                // 关闭Shiro自带的session
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        defaultWebSecurityManager.setSubjectDAO(subjectDAO);
        // 设置自定义Cache缓存
        securityManager.setCacheManager(shiroRedisCacheManager());
        return securityManager;
    }


    /**
     * Filter工厂,设置对应的过滤条件和跳转条件
     * @param securityManager 安全管理器
     * @return 过滤组件
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(org.apache.shiro.mgt.SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //设置安全管理器
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //设置过滤请求
        Map<String,String> filterMap = new LinkedHashMap<>();
        filterMap.put("/system/login/loginPage", "anon");
        filterMap.put("/system/login/loginByUsernameAndPassword", "anon");
        filterMap.put("/**", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
        //未登录跳转地址
        shiroFilterFactoryBean.setLoginUrl("/system/login/loginPage");
        return shiroFilterFactoryBean;
    }

    @Bean
    public CacheManager shiroRedisCacheManager() {
        return new ShiroRedisCacheManager(redisTemplate);
    }
}

三、集成jwt

3.1 依赖版本管理

 <jwt.version>3.3.0</jwt.version>

3.2 依赖引入

        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>${jwt.version}</version>
        </dependency>

3.3 常量設置

package cn.com.wenyl.bs.config.jwt;

/**
 * @author Swimming Dragon
 * @description: 公钥私钥等信息
 * @date 2023年12月06日 13:59
 */
public class JwtConstant {
    /**
     * token过期时间(毫秒)
     */
    public static final long ACCESS_TOKEN_EXPIRE_TIME = 43200000;

    /**
     * 用户登录账号属性名
     */
    public static final String ACCOUNT_PROPERTY_NAME = "username";
    /**
     * token在request header中的属性名
     */
    public static final String REQUEST_HEADER_TOKEN = "BS-WEB-TOKEN";


    /**
     * shiro的token在redis中的缓存key的前缀
     */
    public static final String PREFIX_SHIRO_WEB_TOKEN = "shiro:web_token:";
    /**
     * token不存在或者无效的返回信息
     */
    public static final String TOKEN_IS_INVALID_MSG = "请先登录";
}

3.4 jwt工具类

package cn.com.wenyl.bs.config.jwt;

import cn.com.wenyl.bs.utils.R;
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 com.fasterxml.jackson.databind.ObjectMapper;

import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.Date;

/**
 * @author Swimming Dragon
 * @description: jwt工具类
 * @date 2023年12月06日 13:24
 */
public class JwtUtil {

    /**
     * 构建一个token
     * @param username 用户名
     * @param password 用户密码
     * @return 返回token
     */
    public static String createToken(String username,String password) throws UnsupportedEncodingException {
        Date date = new Date(System.currentTimeMillis() + JwtConstant.ACCESS_TOKEN_EXPIRE_TIME);
        Algorithm algorithm = Algorithm.HMAC256(password);
        return JWT.create().withClaim(JwtConstant.ACCOUNT_PROPERTY_NAME, username).withExpiresAt(date).sign(algorithm);
    }

    /**
     * 校验token
     * @param token token
     * @param username 用户名
     * @param password 用户密码
     * @return 返回token是否合法
     */
    public static boolean verifyToken(String token, String username, String password) {
        try {
            // 根据密码生成JWT效验器
            Algorithm algorithm = Algorithm.HMAC256(password);
            JWTVerifier verifier = JWT.require(algorithm).withClaim(JwtConstant.ACCOUNT_PROPERTY_NAME, username).build();
            // 效验TOKEN
            DecodedJWT jwt = verifier.verify(token);
            return true;
        } catch (Exception exception) {
            return false;
        }
    }

    /**
     * 根据token获取用户名
     * @param token token
     * @return 用户名
     */
    public static String getUsername(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim(JwtConstant.ACCOUNT_PROPERTY_NAME).asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }


    /**
     * response 响应错误
     * @param response 响应
     * @param code 错误编码
     * @param errorMsg 错误信息
     */
    public static void responseError(ServletResponse response, Integer code, String errorMsg) {
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        // issues/I4YH95浏览器显示乱码问题
        httpServletResponse.setHeader("Content-type", "text/html;charset=UTF-8");
        R ret = R.error(code,errorMsg);
        OutputStream os;
        try {
            os = httpServletResponse.getOutputStream();
            httpServletResponse.setCharacterEncoding("UTF-8");
            httpServletResponse.setStatus(code);
            os.write(new ObjectMapper().writeValueAsString(ret).getBytes(StandardCharsets.UTF_8));
            os.flush();
            os.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

3.5 自定义token

package cn.com.wenyl.bs.config.jwt;

import lombok.Data;
import org.apache.shiro.authc.AuthenticationToken;

/**
 * @author Swimming Dragon
 * @description: jwt token
 * @date 2023年12月06日 14:13
 */
@Data
public class JwtToken implements AuthenticationToken {
    /**
     * Token
     */
    private String token;

    public JwtToken(String token) {
        this.token = token;
    }

    @Override
    public Object getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return token;
    }
}

3.6 自定义realm认证器

package cn.com.wenyl.bs.config.jwt;

import cn.com.wenyl.bs.system.entity.SysRole;
import cn.com.wenyl.bs.system.entity.SysUser;
import cn.com.wenyl.bs.system.service.ISysRoleService;
import cn.com.wenyl.bs.system.service.ISysUserService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
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.data.redis.core.RedisTemplate;

import javax.annotation.Resource;
import java.util.List;

/**
 * @author Swimming Dragon
 * @description: TODO
 * @date 2023年12月06日 14:15
 */
@Slf4j
public class JwtRealm extends AuthorizingRealm {
    @Resource
    private ISysUserService userService;
    @Resource
    private ISysRoleService roleService;

    @Resource
    private RedisTemplate<String,Object> redisTemplate;
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String id = (String) principals.getPrimaryPrincipal();
        // 获取角色信息
        List<SysRole> roleList = roleService.getUserRoleList(id);
        //添加角色权限
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        for(SysRole role:roleList){
            simpleAuthorizationInfo.addRole(role.getRoleCode());
        }
        return simpleAuthorizationInfo;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String token = (String) authenticationToken.getCredentials();
        // 解密获得account,用于和数据库进行对比
        String username = JwtUtil.getUsername(token);
        // 帐号为空
        if (StringUtils.isBlank(username)) {
            log.error("Token中帐号为空");
            throw new AuthenticationException("Token中帐号为空");
        }
        // 查询用户是否存在
        SysUser sysUser = userService.getByUserName(username);
        if (sysUser == null) {
            throw new AuthenticationException("帐号"+username+"不存在");
        }
        // 验证token和refreshToken
        Boolean exists = redisTemplate.hasKey(JwtConstant.PREFIX_SHIRO_WEB_TOKEN + username);

        if (JwtUtil.verifyToken(token,username,sysUser.getPassword()) && exists != null && exists) {
            return new SimpleAuthenticationInfo(sysUser, token, getName());
        }
        throw new AuthenticationException("Token已过期)");
    }

    /**
     * 指定当前realm适用的token类型为JwtToken
     * @param authenticationToken the token being submitted for authentication. 认证的token
     * @return 如果类型为JwtToken则返回true,否则返回false
     */
    @Override
    public boolean supports(AuthenticationToken authenticationToken) {
        return authenticationToken instanceof JwtToken;
    }
}

3.7 自定义过滤器

package cn.com.wenyl.bs.config.jwt;

import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpStatus;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @author Swimming Dragon
 * @description: jwt过滤器,用来做token校验
 * @date 2023年12月06日 13:10
 */
@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter{
    public JwtFilter(){}

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        try {
            executeLogin(request, response);
            return true;
        } catch (Exception e) {
            JwtUtil.responseError(response, HttpStatus.SC_UNAUTHORIZED,JwtConstant.TOKEN_IS_INVALID_MSG);
            return false;
        }
    }

    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String token = httpServletRequest.getHeader(JwtConstant.REQUEST_HEADER_TOKEN);
        JwtToken jwtToken = new JwtToken(token);
        getSubject(request, response).login(jwtToken);
        return true;
    }

    /**
     * 对跨域提供支持
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        return super.preHandle(request, response);
    }
}

3.8 shiro配置

修改为如下配置

package cn.com.wenyl.bs.config;

import cn.com.wenyl.bs.config.jwt.JwtFilter;
import cn.com.wenyl.bs.config.jwt.JwtRealm;
import cn.com.wenyl.bs.config.shiro.ShiroRedisCacheManager;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;

import javax.annotation.Resource;
import javax.servlet.Filter;
import java.util.*;

/**
 * @author Swimming Dragon
 * @description: 配置shiro
 * @date 2023年12月05日 9:55
 */
@Configuration
public class ShiroConfig {
    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    @Bean
    public JwtRealm getJwtRealm() {
        return new JwtRealm();
    }

    /**
     * 权限管理,配置主要是Realm的管理认证
     * 需要使用redis存储认证信息,所以,关闭session,重写缓存管理器
     * @param jwtRealm jwt认证管理
     * @return 安全管理器
     */
    @Bean("securityManager")
    public DefaultWebSecurityManager securityManager(JwtRealm jwtRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        List<Realm> list = new ArrayList<>();
        list.add(jwtRealm);
        securityManager.setRealms(list);

        // 关闭Shiro自带的session
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        defaultWebSecurityManager.setSubjectDAO(subjectDAO);
        // 设置自定义Cache缓存
        securityManager.setCacheManager(shiroRedisCacheManager());

        return securityManager;
    }


    /**
     * Filter工厂,设置对应的过滤条件和跳转条件
     * @param securityManager 安全管理器
     * @return 过滤组件
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(org.apache.shiro.mgt.SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //设置安全管理器
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        //设置过滤器
        Map<String, Filter> filterMap = new LinkedHashMap<>();
        filterMap.put("jwtFilter",new JwtFilter());
        shiroFilterFactoryBean.setFilters(filterMap);

        //设置请求规则,登录页面放行,其他页面需要认证
        Map<String,String> filterChainDefinitionMap = new LinkedHashMap<>();
        filterChainDefinitionMap.put("/system/login/loginPage", "anon");
        filterChainDefinitionMap.put("/system/login/loginByUsernameAndPassword", "anon");
        filterChainDefinitionMap.put("/**", "jwtFilter");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        //未登录跳转地址
        shiroFilterFactoryBean.setLoginUrl("/system/login/loginPage");
        return shiroFilterFactoryBean;
    }

    @Bean
    public CacheManager shiroRedisCacheManager() {
        return new ShiroRedisCacheManager(redisTemplate);
    }
}

3.9 修改登录接口

package cn.com.wenyl.bs.system.controller;

import cn.com.wenyl.bs.config.jwt.JwtConstant;
import cn.com.wenyl.bs.config.jwt.JwtUtil;
import cn.com.wenyl.bs.system.entity.SysUser;
import cn.com.wenyl.bs.system.service.ISysUserService;
import cn.com.wenyl.bs.utils.R;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.util.concurrent.TimeUnit;


/**
 * @author Swimming Dragon
 * @description: 登录接口
 * @date 2023年12月05日 13:23
 */
@Slf4j
@Controller
@RequestMapping("/system/login")
@Api(tags="登录")
public class LoginController {
    @Resource
    private ISysUserService sysUserService;
    @Resource
    private RedisTemplate<String,Object> redisTemplate;

    @GetMapping("/loginPage")
    @ApiOperation(value="登录-跳转到登录页", notes="登录-跳转到登录页")
    public String login(){
        return "login";
    }

    @GetMapping("/indexPage")
    @ApiOperation(value="登录-跳转到首页", notes="登录-跳转到首页")
    public String index(){
        return "index";
    }

    @PostMapping("/loginByUsernameAndPassword")
    @ApiOperation(value="登录-跳转到登录页", notes="登录-跳转到登录页")
    @ResponseBody
    public R<?> login(@RequestParam("username") String username,
                      @RequestParam("password") String password) {
        Boolean exist = redisTemplate.hasKey(JwtConstant.PREFIX_SHIRO_WEB_TOKEN+username);
        if(exist != null && exist){
            return R.ok("已登录,不要重复登录");
        }
        // 获取用户
        SysUser sysUser = sysUserService.getByUserName(username);
        if(!password.equals(sysUser.getPassword())){
            return R.error("密码错误,回到登录页");
        }
        // 获取token
        String token;
        try {
            token = JwtUtil.createToken(username, password);
        } catch (UnsupportedEncodingException e) {
            log.error(e.getMessage());
            return R.error(e.getMessage());
        }
        redisTemplate.opsForValue().set(JwtConstant.PREFIX_SHIRO_WEB_TOKEN+username,token, JwtConstant.ACCESS_TOKEN_EXPIRE_TIME, TimeUnit.MICROSECONDS);
        return R.ok(token);
    }


    @RequestMapping(value = "/logout")
    public R<?> logout(HttpServletRequest request) {
        //用户退出逻辑
        String token = request.getHeader(JwtConstant.REQUEST_HEADER_TOKEN);
        if(StringUtils.isEmpty(token)) {
            return R.error("退出登录失败!");
        }
        String username = JwtUtil.getUsername(token);
        SysUser sysUser = sysUserService.getByUserName(username);
        if(sysUser!=null) {
            redisTemplate.opsForValue().getOperations().delete(JwtConstant.PREFIX_SHIRO_WEB_TOKEN+username);
            //调用shiro的logout
            SecurityUtils.getSubject().logout();

            log.info(" 用户名:  "+username+",退出成功! ");
            return R.ok("退出登录成功!");
        }else {
            log.info(" 用户名:  "+username+"退出登录失败,用户不存在!");
            return R.error("退出登录失败,用户不存在!");
        }
    }
}

3.10 测试

访问首页,提示401

image.png

进入登录页,登录成功后,返回token

image.png


标题:spring-boot集成shiro和jwt
作者:wenyl
地址:http://www.wenyoulong.com/articles/2023/12/05/1701755424228.html