Springboot+JWT+Shiro集成完全版(带测试示例)

Posted 村长

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Springboot+JWT+Shiro集成完全版(带测试示例)相关的知识,希望对你有一定的参考价值。

 

相信大家已经对shiro,jwt有基本的概念了,不熟悉的可以看下

jwt:https://blog.csdn.net/Goligory/article/details/104400381

对于shiro等会我贴上代码然后简单分析下

maven引入

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-starter</artifactId>
        </dependency>
        <!-- shiro-redis -->
        <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis</artifactId>
        </dependency>

 

import com.mtgg.laoxiang.common.constant.CommonConstant;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;

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

/**
 * @Author: lǎo xiāng
 * @Date: 2021/2/4 17:56
 * @Describe: 鉴权登录拦截器
 */
@Slf4j
@Component
public class JwtFilter extends BasicHttpAuthenticationFilter 

    private boolean allowOrigin = true;

    public JwtFilter()
    public JwtFilter(boolean allowOrigin)
        this.allowOrigin = allowOrigin;
    

    /**
     * 执行登录认证
     *
     * @param request
     * @param response
     * @param mappedValue
     * @return
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) 
        try 
            executeLogin(request, response);
            return true;
         catch (Exception e) 
            throw new AuthenticationException("Token失效,请重新登录", e);
        
    

    /**
     *
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) 
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String token = httpServletRequest.getHeader(CommonConstant.X_ACCESS_TOKEN);

        JwtToken jwtToken = new JwtToken(token);
        // 提交给realm进行登入,如果错误他会抛出异常并被捕获
        getSubject(request, response).login(jwtToken);
        // 如果没有抛出异常则代表登入成功,返回true
        return true;
    

    /**
     * 对跨域提供支持
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception 
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        if(allowOrigin)
            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"));
            // 是否允许发送Cookie,默认Cookie不包括在CORS请求之中。设为true时,表示服务器允许Cookie包含在请求中。
            httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
        
        // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) 
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        
        //多租户用到
//        String tenant_id = httpServletRequest.getHeader(CommonConstant.TENANT_ID);
//        TenantContext.setTenant(tenant_id);
        return super.preHandle(request, response);
    

这个类主要做的事:当有访问携带token过来的时候会走isAccessAllowed认证,由executeLogin交给shiro进行验证

那么验证过程中shiro和jwt token是如何进行关联的呢?看下面

import org.apache.shiro.authc.AuthenticationToken;

/**
 * @Author: lǎo xiāng
 * @Date: 2021/2/4 17:56
 * @Describe: 实现AuthenticationToken 使Realm的doGetAuthenticationInfo能够获取到token进行验证
 */
public class JwtToken implements AuthenticationToken 
	
	private static final long serialVersionUID = 1L;
	private String token;
 
    public JwtToken(String token) 
        this.token = token;
    
 
    @Override
    public Object getPrincipal() 
        return token;
    
 
    @Override
    public Object getCredentials() 
        return token;
    
AuthenticationToken是shiro-core包下的接口,实现后可以用getPrincipal获取到我们的token,这样shiro就拿到了

 

下面再加入配置对接口的过滤等配置,其中可以设置登录,过滤路径,注意看要添加自定义Filter,JwtFilter就是在此时被加载生效

import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;

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

/**
 * @Author: lǎo xiāng
 * @Date: 2021/2/4 17:57
 * @Describe: shiro 配置类
 */
@Slf4j
@Configuration
public class ShiroConfig 

    /**
     * Filter Chain定义说明
     *
     * 1、一个URL可以配置多个Filter,使用逗号分隔
     * 2、当设置多个过滤器时,全部验证通过,才视为通过
     * 3、部分过滤器可指定参数,如perms,roles
     */
    @Bean
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) 
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 必须设置 SecurityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // setLoginUrl 如果不设置值,默认会自动寻找Web工程根目录下的"/login.jsp"页面 或 "/login" 映射
        shiroFilterFactoryBean.setLoginUrl("/login/notLogin");
        // 设置无权限时跳转的 url;
        shiroFilterFactoryBean.setUnauthorizedUrl("/login/notRole");

        // 设置拦截器
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        //游客,开发权限   TODO 暂时设置所有都不拦截
        filterChainDefinitionMap.put("/bg/**", "anon");
        filterChainDefinitionMap.put("/start/**", "anon");
        filterChainDefinitionMap.put("/test/**", "anon");
        //swagger接口权限 开放
        filterChainDefinitionMap.put("/swagger-ui.html", "anon");
        filterChainDefinitionMap.put("/doc.html", "anon");
        filterChainDefinitionMap.put("/webjars/**", "anon");
        filterChainDefinitionMap.put("/v2/**", "anon");
        filterChainDefinitionMap.put("/swagger-resources/**", "anon");

//        filterChainDefinitionMap.put("/elastic/**", "anon");
        //用户,需要角色权限 “user”
        filterChainDefinitionMap.put("/user/**", "roles[user]");
        //管理员,需要角色权限 “admin”
        filterChainDefinitionMap.put("/admin/**", "roles[admin]");
        //其余接口一律拦截
        //主要这行代码必须放在所有权限设置的最后,不然会导致所有 url 都被拦截
        filterChainDefinitionMap.put("/**", "authc");

        //添加自定义过滤器并取名jwt
        Map<String, Filter> map = new HashMap<>(1);
        map.put("jwt",new JwtFilter());
        shiroFilterFactoryBean.setFilters(map);
//        //所有请求通过我们自己的JWT Filter
        filterChainDefinitionMap.put("/**", "jwt");

        // 访问 /unauthorized/** 不通过JWTFilter
        filterChainDefinitionMap.put("/unauthorized/**", "anon");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        log.info("Shiro拦截器工厂类注入成功");
        return shiroFilterFactoryBean;
    

    @Bean("securityManager")
    public DefaultWebSecurityManager securityManager(CustomRealm customRealm) 
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(customRealm);
        return securityManager;
    

    //将自己的验证方式加入容器
    @Bean
    public CustomRealm myShiroRealm() 
        CustomRealm customRealm = new CustomRealm();
        return customRealm;
    

    /**
     * 下面的代码是添加注解支持
     * @return
     */
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() 
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        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;
    

好了,shiro+jwt+springboot已经配置好了,那么如何使用呢?先看登录

    @ApiOperation(value = "登录注册", notes = "校验,注册,生成token")
    @PostMapping("/login")
    public Result<LoginDTO> login(@RequestBody LoginReq loginReq) 
        log.info("登录注册:", JSONObject.toJSONString(loginReq));
        String phone = loginReq.getPhone();
        Result check = check(loginReq);
        if (!check.isSuccess()) 
            return check;
        

        Users user = usersService.login(loginReq);

        AuthInfo authInfo = new AuthInfo();
        authInfo.setPhone(phone);
        authInfo.setUserId(user.getId());
        authInfo.setUsername(user.getUsername());
        //TODO 暂时不用shiro验证,后续加入
        String token = JwtUtil.signInfo(authInfo, phone);
        boolean set = redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, String.valueOf(user.getId()), JwtUtil.EXPIRE_SECOND - 1);
        LoginDTO loginDTO = new LoginDTO();
        loginDTO.setUsername(user.getUsername());
        loginDTO.setHeadImg(user.getHeadImg());
        loginDTO.setToken(token);
        log.info("登录注册返回loginDTO:", loginDTO);
        return Result.success(loginDTO);
    

这里就联系上了,check是进行一些检验,通过后可以用userService.login来检查登录注册,没有可以创建账号

账号有了,也通过了,最后生成token,可以封装一个对象放到value中,这样扩展性比较好,注意redis的时间要比token的时间短一点

登出

    @ApiOperation(value = "登出")
    @GetMapping("/logout")
    public Result logout(HttpServletRequest request)
        String token = request.getHeader(KeyConstant.TOKEN);
        Subject lvSubject= SecurityUtils.getSubject();
        lvSubject.logout();
        redisUtil.del(CommonConstant.PREFIX_USER_TOKEN + token);
        return Result.success();
    

权限验证如下,其中什么角色还是权限在 CustomRealm的 doGetAuthorizationInfo 中加进去


@RequiresRoles(logical = Logical.OR, value = "user", "admin")
有user或admin角色

@RequiresPermissions("vip")
有vip权限

                不知道看没看到我的注释,CustomRealm这个类有一个注入注释,如果使用UserService会导致UserService的事务失效,可以新建一个类来查询处理:也就是说如果注入了会导致UserServiceImpl中@Transationl失效,具体为什么可以查一下,本人没细看

               解决方案有二

                 1.加上@Lazy注解,延迟注入

                 2.用另一个Service专门来做注入处理

 

好,那么具体是什么执行流程呢?我给大家演示一下

首先,当然是登录了,登录后会返回一个token,后续请求中按约定携带token

可以看到先走的是自定义JwtFilter的验证方法

接下来走的是JwtToken的构造方法赋值token,这样就给了shiro;

接下来执行

getSubject(request, response).login(jwtToken);

可以看到开始通过CustomRealm验证了,首先拿到innfo,验证token有效性,中间其实可以加入其它验证

这里的设计是为了解决用户操作中失效的问题,具体解释看代码更详细,如果大于3小时可以不做延长时间处理

校验token有效性表示:如果redis中有token,jwt中token失效了,那么重新生成并设置时间(当然正常来说不会,因为redis时间可以设置比token少一点使jwt中的token一定晚于redistoken失效)

权限的我就不执行了

祝我们不忘初心,方得始终

 

 

 

 

 

 

 

 

 

 

以上是关于Springboot+JWT+Shiro集成完全版(带测试示例)的主要内容,如果未能解决你的问题,请参考以下文章

带有 JWT 的 Spring Boot 和 Apache Shiro - 我使用正确吗?

springboot shiro和freemarkervuejs/element-ui集成之控制按钮权限完全参考手册

SpringBoot2.0+Shiro+JWT 整合

springboot+shiro+jwt实现登录

SpringBoot整合Shiro+JWT实现认证及权限校验

SpringBoot整合Shiro+JWT实现认证及权限校验