当前位置: 首页 > 工具软件 > JJWT > 使用案例 >

spring boot整合shiro+jjwt

皇甫卓君
2023-12-01

前言

本篇文章将教大家在 shiro + springBoot 的基础上整合 JJWT (JSON Web Token)

JWT

JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用 JWT 在用户和服务器之间传递安全可靠的信息。

我们利用一定的编码生成 Token,并在 Token 中加入一些非敏感信息,将其传递。

一个完整的 Token :
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0.rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM

在本项目中,我们规定每次请求时,需要在请求头中带上 token ,通过 token 检验权限,如没有,则说明当前为游客状态(或者是登陆 login 接口等)

maven

下面是本次整合使用到的工具包

		<!--JjWT-->
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt</artifactId>
			<version>0.9.1</version>
		</dependency>

		<!-- shiro -->
		<dependency>
			<groupId>org.apache.shiro</groupId>
			<artifactId>shiro-spring</artifactId>
			<version>1.4.0</version>
		</dependency>
		
		<!--整合mybatis-plus-->
		<dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatis-plus-boot-starter</artifactId>
			<version>3.4.1</version>
		</dependency>

JWTUtil

我们自定义一个jwtutil工具类实现token生成和解析

public class JWTUtil {

    // 过期时间5分钟
    private static final long EXPIRE_TIME = 5*60*1000;

    //私钥
    private static final String SECRETKEY = "imcode";

    /**
     * token解析
     * @param token token
     * @return io.jsonwebtoken.Claims
     */
    public static Claims verify(String token){
        Claims claims;
        try {
             claims = Jwts.parser()
                    .setSigningKey(DatatypeConverter.parseBase64Binary(SECRETKEY))
                    .parseClaimsJws(token)
                    .getBody();
        }catch (ExpiredJwtException e){
            throw new AuthenticationException("token 过期"+e.getMessage());
        }catch (UnsupportedJwtException e){
            throw new AuthenticationException("token 无效"+e.getMessage());
        }catch (MalformedJwtException e){
            throw new AuthenticationException("token 格式错误"+e.getMessage());
        }catch (SignatureException e){
            throw new AuthenticationException("token 签名无效"+e.getMessage());
        }catch (IllegalArgumentException e){
            throw new AuthenticationException("token 参数异常"+e.getMessage());
        }catch (Exception e){
            throw new AuthenticationException("token 令牌错误"+e.getMessage());
        }
        return claims;
    }

    /**
     * 生成签名,5min后过期
     * @param username 用户名
     * @return 加密的token
     */
    public static String sign(String username) {
       JwtBuilder jwt = Jwts.builder();
       //设置token唯一标识
        jwt.setId(UUID.randomUUID().toString());
        //设置主题
        jwt.setSubject(username);
        //设置签发者
        jwt.setIssuer(SECRETKEY);
        //设置签发日期
        jwt.setIssuedAt(new Date());
        //设置过期时间
        jwt.setExpiration(new Date(System.currentTimeMillis() + EXPIRE_TIME));
        //私钥
        byte[] secretKeyBytes = DatatypeConverter.parseBase64Binary(SECRETKEY);
        //签名
        jwt.signWith(SignatureAlgorithm.HS256,secretKeyBytes);
        return jwt.compact();
    }
}

JWTToken

自定义一个token类替代shiro本身的userPasswordToken

public class JWTToken implements AuthenticationToken {

    private String token;

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

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

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

JWTFilter

然后自定义一个过滤器将拦截到的请求用我们自己的逻辑处理

@Slf4j
public class JWTFilter extends AccessControlFilter {
    // 登录标识
    private static String LOGIN_SIGN = "Authorization";

    /**
     * 是否允许通过,因为是无状态所以默认不通过,去自动登陆,返回false,调用onAccessDenied方法
     * @param request
     * @param response
     * @param mappedValue
     * @return boolean
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        return false;
    }

    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        //使用token登陆
        try {
            HttpServletRequest req = (HttpServletRequest) servletRequest;
            String authorization = req.getHeader(LOGIN_SIGN);
            JWTToken token = new JWTToken(authorization);
            // 提交给realm进行登入,如果错误他会抛出异常并被捕获
            getSubject(servletRequest, servletResponse).login(token);
        }catch (AuthenticationException e){
            response401(e.getMessage(), servletResponse);
            return false;
        }

        // 如果没有抛出异常则代表登入成功,返回true
        return true;
    }

    /**
     * 对跨域提供支持
     */
    @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"));
        // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }

    /**
     * 将非法请求跳转到 /401
     */
    private void response401(String erroInfo, ServletResponse resp) {
        try {
            HttpServletResponse response = (HttpServletResponse) resp;
            response.setContentType("json/html;charset=utf8");
            response.setCharacterEncoding("utf8");
            PrintWriter pw = response.getWriter();
            Msg msg = new Msg();
            msg.setStatus("401");
            msg.setMessage(erroInfo);
            String result = JSON.toJSONString(msg);
            pw.write(result);
        } catch (IOException e) {
            log.error(e.getMessage());
        }
    }
}

MyRealm

自定义一个realm处理自己的登陆认证以及授权逻辑

@Service
public class MyRealm extends AuthorizingRealm {
    @Autowired
    private UserService userService;

    @Autowired
    private ResourceService resourceService;

    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

        Claims claims = (Claims) principalCollection.getPrimaryPrincipal();

        String username = claims.getSubject();

        if ("admin".equals(username)) {
            Set<String> permissions = resourceService.list().stream().map(ResourceEntity::getPermission).collect(Collectors.toSet());
            info.addStringPermissions(permissions);
            info.addRole("admin");
            return info;
        }
        //2.查询数据库用户
        QueryWrapper<UserEntity> wrapper = new QueryWrapper<>();
        wrapper.eq("username", username);
        UserEntity user = userService.getOne(wrapper);
        String userId = user.getUserId();
        Set<String> permissions = userService.getPermissions(userId);

        info.addStringPermissions(permissions);
        return info;
    }

    //登陆认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) {
        //1.获取用户输入的账户信息
        String token = (String) authenticationToken.getPrincipal();

        Claims claims = JWTUtil.verify(token);

        return new SimpleAuthenticationInfo(claims, token, getName());
    }

    /**
     * 找它的原因是这个方法返回true
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JWTToken;
    }
}

ShiroConfig

配置shiro内容,因为使用jwt方式登陆是无状态的,所以配置中禁用了session会话

@Configuration
public class ShiroConfig {
    //1.创建ShiroFilterFactory
    @Bean("shiroFilter")
    public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();

        // 添加自己的过滤器并且取名为jwt
        Map<String, Filter> filterMap = new HashMap<>();
        filterMap.put("jwt", new JWTFilter());
        factoryBean.setFilters(filterMap);

        factoryBean.setSecurityManager(securityManager);
        factoryBean.setUnauthorizedUrl("/user/unauthorized");

        /*
         * 自定义url规则
         * http://shiro.apache.org/web.html#urls-
         */
        Map<String, String> filterRuleMap = new LinkedHashMap<>();
        filterRuleMap.put("/user/login","anon");
        filterRuleMap.put("/user/unauthorized", "anon");
        filterRuleMap.put("/user/logout", "anon");
        // 所有请求通过我们自己的JWT Filter
        filterRuleMap.put("/**", "jwt");
        // 访问401和404页面不通过我们的Filter

        factoryBean.setFilterChainDefinitionMap(filterRuleMap);
        return factoryBean;
    }

    @Bean
    public CredentialMatcher credentialMatcher() {
        //自定义的密码比较器
        return new CredentialMatcher();
    }

    @Bean("securityManager")
    public DefaultWebSecurityManager getManager(MyRealm realm) {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
//        realm.setCredentialsMatcher(credentialMatcher());
        // 使用自己的realm
        manager.setRealm(realm);

        /*
         * 关闭shiro自带的session,详情见文档
         * http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
         */
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        manager.setSubjectDAO(subjectDAO);

        return manager;
    }

    /**
     * 下面的代码是添加注解支持
     */
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        // 强制使用cglib,防止重复代理和可能引起代理出错的问题
        // https://zhuanlan.zhihu.com/p/29161098
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

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

MD5加密

到此shiro已经整合完成,再整合一下md5密码加密功能

public class MD5Util {
    /**
     * 密码md5加密
     * @param password 原始密码
     * @param salt 盐
     * @return java.lang.String
     */
    public static String createPasswd(String password,String salt){
        return new Md5Hash(password,salt,2).toString();
    }
}

controller添加用户和登陆接口

@RestController
@RequestMapping(value = "/user")
@Api(
        tags = {"系统用户管理"},
        value = "系统用户管理",
        consumes = MediaType.APPLICATION_JSON_VALUE,
        produces = MediaType.APPLICATION_JSON_VALUE
)
public class UserController {
    @Autowired
    private UserService userService;

    @ApiOperation(value = "用户登陆")
    @PostMapping("/login")
    public Msg userLogin(@NotNull @RequestBody UserLoginDTO userLoginDTO) {
        QueryWrapper<UserEntity> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("username",userLoginDTO.getUsername());
        queryWrapper.eq("deleted",0);
        UserEntity user = userService.getOne(queryWrapper);
        if (null == user) throw new UnknownAccountException("用户名和密码错误");
        if (user.getStatus() == 1) throw new UnknownAccountException("用户被禁用");
        if (user.getStatus() == 2) throw new UnknownAccountException("用户被锁定");
		
		//将传入密码加密与数据库密码对比
        String password = MD5Util.createPasswd(userLoginDTO.getPassword(),user.getSalt());
        if (!password.equals(user.getPassword())) {
            throw new UnknownAccountException("用户名和密码错误");
        }

		//签发token返回
        String token = JWTUtil.sign(userLoginDTO.getUsername());
        Map<String, String> resultMap = new HashMap<>();
        resultMap.put("Authorization", token);
        return Msg.success(resultMap);
    }

    @Transactional
    @RequiresRoles(value = "admin")
    @PostMapping("/add")
    @ApiOperation(value = "添加用户")
    public Msg addUser(@RequestBody UserAddDTO userAddDTO){
    	//使用uuid随机生成6位的盐存入数据库
        String uuId = UUID.randomUUID().toString();
        String salt = uuId.substring(uuId.length()-6);
        
        //加密密码
        String password = MD5Util.createPasswd("2wsx@WSX",salt);
        userAddDTO.setSalt(salt);
        userAddDTO.setPassword(password);
        String result = userService.addUser(userAddDTO);
        return Msg.success(result);
    }

	//权限校验
    @RequiresPermissions(logical = Logical.AND, value = {"userlist:view", "userlist:edit"})
    @GetMapping(value = "/list")
    public Msg<PageInfo<UserEntity>> getUserList(UserQueryDTO queryDTO) {
        PageInfo<UserEntity> pageInfo = userService.getUserList(queryDTO);
        return Msg.success(pageInfo);
    }

    @GetMapping("/unauthorized")
    public Msg unauthorized() {
        CodeMessage codeMessage = CodeMessage.UNAUTHORIZED;
        Msg msg = new Msg();
        msg.setStatus(codeMessage.getStatus());
        msg.setMessage(codeMessage.getMessage());
        return msg;
    }
}

以上所有使用到的userService方法自己实现自己的逻辑,至此整合shiro与jjwt完成,仅供参考。

 类似资料: