springboot+shiro+redis+jwt模块化开发登录权限校验


之前在公司的时候一直接触到这个东西,由于是同事写的,而且没有仔细的把流程实现一遍,总是有些一知半解的样子,就自己做了一个小的demo来过一遍流程。我这里使用的是模块化的开发,只涉及到两个服务,一个是登录的一个管理的并没有用到分布式网关那些。相对于来说没有这么复杂,服务器的之前的交互也只是使用到restTemplate而已,所以仅供参考吧。

1.demo项目结构

gitee源码地址:https://gitee.com/bai-yaofeng/springboot002
请添加图片描述
service模块和common模块主要都是存放公共信息的,service存放的是pojo,这里就不展开了。
而sso模块则是定义登录规则的,主要存放shiro认证和权限校验,manager模块是管理模块的

1.1.导入依赖

这里把全部粘下去了,自己看对应版本就行

  <!--定义版本号-->
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <mybatis-plus.version>3.3.2</mybatis-plus.version>
        <velocity.version>2.0</velocity.version>
        <lombok.version>1.16.20</lombok.version>
        <swagger.version>2.7.0</swagger.version>
        <jodatime.version>2.10.1</jodatime.version>
        <commons-fileupload.version>1.3.1</commons-fileupload.version>
        <commons-beansutils.version>1.9.3</commons-beansutils.version>
        <commons-io.version>2.6</commons-io.version>
        <httpclient.version>4.5.2</httpclient.version>
        <fastjson.version>1.2.28</fastjson.version>
        <poi.version>3.17</poi.version>
        <mysql.version>8.0.11</mysql.version>
        <druid.version>1.1.10</druid.version>
        <redis.version>2.9.2</redis.version>
        <shiro-spring.version>1.4.0</shiro-spring.version>
        <shiro-redis.version>2.4.2.1-RELEASE</shiro-redis.version>
        <shiro-spring-boot-web-starter.version>1.6.0</shiro-spring-boot-web-starter.version>
        <jwt.version>3.2.0</jwt.version>
        <pinyin.version>2.5.0</pinyin.version>
    </properties>


    <dependencies>
        <!--mybatis-plus 持久层-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>

        <!-- velocity 模板引擎, Mybatis Plus 代码生成器需要 -->
        <dependency>
            <groupId>org.apache.velocity</groupId>
            <artifactId>velocity-engine-core</artifactId>
            <version>${velocity.version}</version>
        </dependency>


        <!-- druid数据库连接池 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>${druid.version}</version>
        </dependency>

        <!-- Mysql -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql.version}</version>
        </dependency>

        <!-- Lombok 插件 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
        </dependency>

        <!-- redis依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>${redis.version}</version>
        </dependency>

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

        <!--commons-io-->
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>${commons-io.version}</version>
        </dependency>

        <dependency>
            <groupId>commons-beanutils</groupId>
            <artifactId>commons-beanutils</artifactId>
            <version>${commons-beansutils.version}</version>
        </dependency>
        
				 <!--springboot整合shiro-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-starter</artifactId>
            <version>1.4.1</version>
        </dependency>

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-ehcache</artifactId>
            <version>1.4.1</version>
        </dependency>

1.2. 登录服务

整合登录模块的主要流程是:
自定义shiro配置类

1.配置shiro,配置其内置过滤,进行自定义的JWT做拦截器进行拦截校验
2.配置SecurityManager,并且将安全管理器作为参数交给shiroFilter管理
3.配置自定义的realm(用来定义验证和权限的),作为参数交给SecurityManager管理,实现redis缓存处理

ShiroConfig配置类

/**
 * shiro配置类
 */
@Configuration
public class ShiroConfig {


    /**
     * 步骤1:配置shiro,将所有请求提交给shiro去管理
     * 添加shiro自带的内置过滤器
     *   常用过滤器:
     *     anon:无需认证(登录)即可访问
     *     authc:必须认证才可以访问
     *     user:如果使用rememberMe的功能可以直接访问
     *     perms:该资源必须获得资源权限才能使用
     *     role:该资源必须得到用户角色权限才能访问
     * @param defaultWebSecurityManager 安全管理器
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //设置安全管理器
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
        //这里不使用shiro自带的拦截器,使用自定义的jwt来进行拦截
        HashMap<String, Filter> filterMap = new HashMap<>();
        filterMap.put("jwt", new JwtFilter());
        shiroFilterFactoryBean.setFilters(filterMap);
        //设置公共资源进行放行,不需要验证
        HashMap<String, String> map = new HashMap<>();
        map.put("/user/login", "anon");  //anno设置为公共资源,不需要认证,注意不受限的资源要放在上边
        map.put("/user/registry", "anon");  //用户通过注册页面,注册信息跳转到注册接口,需要放行
        map.put("/user/getAuthorizationInfo","anon"); //根据token校验信息
        //拦截请求使用我们自己定义的拦截器
        map.put("/**", "jwt");
        //将规则交给shiro
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        return shiroFilterFactoryBean;
    }

    /**
     * 步骤2.配置SecurityManager,并且将安全管理器作为参数交给shiroFilter管理 web环境下 创建DefaultWebSecurityManager
     * @param realm  自定义realm
     * @return
     */
    @Bean
    public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("realm") Realm realm){
        //给安全管理器设置自定义的realm,从bean中获取到的传递过来的
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        defaultWebSecurityManager.setRealm(realm);
        return defaultWebSecurityManager;
    }

    /**
     * 3.配置自定义的realm(用来定义验证和权限的),作为参数交给SecurityManager管理,实现redis缓存处理
     * @return
     */
    @Bean("realm")
    public Realm getRealm(){
        //创建自定的realm
        MyRealm myRealm=new MyRealm();
        //开启缓存管理避免了多次反复查询数据库
        myRealm.setCacheManager(new RedisCacheManager()); //自定义的redis缓存也可以通过实现CacheManager做shiro缓存
        myRealm.setCachingEnabled(true);   //开启缓存
        myRealm.setAuthenticationCachingEnabled(true);  //开启认证缓存
        myRealm.setAuthenticationCacheName("authenticationCache");  //设置认证缓存的名称,方便查找 (默认名称:authentication + 自定义realm名称)
        myRealm.setAuthorizationCachingEnabled(true);   //开启授权缓存
        myRealm.setAuthorizationCacheName("authorizationCache");  //设置授权缓存名称*/
        return myRealm;
    }

自定义realm,这里主要定义授权和校验的规则

/**
 * 自定义realm,授权的认证
 */
public class MyRealm extends AuthorizingRealm {


    public  String JWT_TOKEN = "token_jwt_";

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private UserService userService;

    @Autowired
    private UserRoleService userRoleService;  //用户-角色表

    @Autowired
    private RoleService roleService;  //角色表

    @Autowired
    private PermissionService permissionService; //权限表

    @Autowired
    private RolePermissionService rolePermissionService;  //角色-用户表


    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    /**
     * 授权
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //TODO 授权先不做,后续补充
        System.out.println("-------执行权限逻辑------");
        //TODO 获取当前登录用户的账号名,后续可修改成mybatis中sql连接的方式
        String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();
        //查询当前的用户角色
        User username = userService.getOne(new QueryWrapper<User>().eq("username", primaryPrincipal));
        List<UserRole> userRoles = userRoleService.list(new QueryWrapper<UserRole>().eq("user_id", username.getUId()));
        if (userRoles.size()!=0){
            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
            userRoles.forEach(userRole->{
                Role role = roleService.getOne(new QueryWrapper<Role>().eq("r_id", userRole.getRoleId()));
                simpleAuthorizationInfo.addRole(role.getRoleName());  //添加角色给shiro
                //根据角色id,查询多个权限信息添加到shiro中去
                List<RolePermission> rolePermissionList = rolePermissionService.list(new QueryWrapper<RolePermission>().eq("role_id", role.getRId()));
                rolePermissionList.forEach(rolePermission -> {
                    Permission permission = permissionService.getOne(new QueryWrapper<Permission>().eq("pId", rolePermission.getPermissionId()));
                    simpleAuthorizationInfo.addStringPermission(permission.getPermissionName()); //添加权限代码给shiro
                });
            });
            return simpleAuthorizationInfo;
        }
        return null;
    }


    /**
     * 认证
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @SneakyThrows
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException{
        System.out.println("-------执行认证逻辑------");
        String token = (String) authenticationToken.getPrincipal();
        //获取到的token是jwt加密后的token.这里需要去解析判断是否符合或者为空,符合规则则继续进行流程
        DecodedJWT decodedJWT = (DecodedJWT) jwtUtil.verifyToken(token).getData().get("jwt");
        if (decodedJWT == null){
            throw new MyException(ExceptionEnums.INVALID_Token);
        }
        String userName = String.valueOf(decodedJWT.getClaim("userName").asString());
        //根据用户名查询数据库信息,不想进行数据库操作的话可直接用jwt存储的信息的来判断
        User user = userService.getOne(new QueryWrapper<User>().eq("username", userName));
        if (user == null) {
            throw new UnknownAccountException("账户不存在");
        }
        //获取redis中对应用户的token信息,用户登录的时候会在redis中存储一个后缀名为username的jwt
        String redisUserToken = (String) redisTemplate.opsForValue().get(JWT_TOKEN+userName);
        try{
            jwtUtil.verifyToken(token);
        }catch (Exception e){
            e.printStackTrace();
        }
        if (redisUserToken == null) {
            throw new MyException(ExceptionEnums.INVALID_Token);
        }
        return new SimpleAuthenticationInfo(user.getUsername(), authenticationToken.getCredentials().toString(), this.getName());
    }


    /**
     * 建议重写此方法,提供唯一的缓存Key
     */
    @Override
    protected Object getAuthorizationCacheKey(PrincipalCollection principals) {
        String userName = (String) principals.getPrimaryPrincipal();
        return userName;
    }

    /**
     * 建议重写此方法,提供唯一的缓存Key
     */
    @SuppressWarnings("unchecked")
    @Override
    protected Object getAuthenticationCacheKey(PrincipalCollection principals) {
        String userName = (String) principals.getPrimaryPrincipal();
        return userName;
    }

}

JwtFilter,自定义JWT拦截器

具体想知道jwt的话可上网了解相关知识,简单来讲就是一个可以包含对象信息的加密串,由三部分组合而成,减少数据库校验的次数

/**
 * jwt自定义拦截器
 */
@Slf4j
@Component
public class JwtFilter extends BasicHttpAuthenticationFilter {


    @SneakyThrows
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        //判断请求的请求头是否带上 "Token"
        String jwtToken = ((HttpServletRequest) request).getHeader("token");
        if (jwtToken != null) {
            //有token,从redis中获取,看是否为已退出用户的token
            RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate");
            if (redisTemplate.opsForValue().get("logout_" + jwtToken) != null) {
                System.out.println(redisTemplate.opsForValue().get("logout_" + jwtToken));
                System.out.println("退出用户的token在redis中存在");
                String result = ResultException.getResult("400", "token无效");
                response.setContentType("application/json;charset=utf-8");
                response.getWriter().print(result);
            }
            //如果存在,则进入 executeLogin 方法执行登入,检查 token 是否正确
            log.info("token存在");
            try {
                executeLogin(request, response);
                return true;
            } catch (Exception e) {
                /*//统一定义成无效token
                String result = ResultException.getResult("400", "无效token");
                response.setContentType("application/json;charset=utf-8");
                response.getWriter().print(result);*/
                return false;
            }
        }
        //测试,如果不存在token的话,说明当前用户并没有进行登录,则返回错误信息,提示必须先登录再进行访问
        String result = ResultException.getResult("401", "当前并未登录,无法访问");
        response.setContentType("application/json; charset=UTF-8");
        response.getWriter().write(result);
        return false;
        //如果请求头不存在 Token,则可能是执行登陆操作或者是游客状态访问,无需检查 token,直接返回 true
        //return true;
    }

    /**
     * 将非法请求跳转到 /filterError/**中
     */
    private void responseError(ServletResponse response, int code, String message) {
        try {
            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
            //设置编码,否则中文字符在重定向时会变为空字符串
            message = URLEncoder.encode(message, "UTF-8");
            //如果有项目名称路径记得加上
            httpServletResponse.sendRedirect("/filterError/" + code + "/" + message);

        } catch (IOException e1) {
            log.error(e1.getMessage());
        }
    }

    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String token = httpServletRequest.getHeader("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 = 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(org.springframework.http.HttpStatus.OK.value());
            return false;
        }

        return super.preHandle(request, response);
    }
}

实现shiro+jwt+redis最主要的就是这三个类,其中还有一些异常处理类和工具类,需要的话可以去源码中查看
主要实现流程就是:用户注册的时候生成一个加密的jwt,里面包含用户的username,以及用在redis中存储一个以用户名结尾的key值,如(token_jwt_xiaobai),然后每次访问其他接口的时候加上一个token,这个token就是redis里面存储的那个jwt串。接口会给shiro拦截,交给自定义的realm去做认证校验,根据对比传入的token和redis存储的token,来判断是否一致,以及token的合法性,是否过期等情况,进行校验

1.3管理服务

管理服务的话主要是编写自己主要的业务逻辑,每个接口都要由sso模块进行认证和权限的校验。

由于我这里用的不是分布式的,所以调用拦截的时候不能利用网关来实现,只能自己在定义过一个拦截器,把拦截请求下来交给sso去做校验

LoginInterceptor管理端拦截器

/**
 * 拦截器,拦截请求,将请求自带的jwtToken拿去sso那里去做校验
 */
@Component
public class LoginInterceptor implements HandlerInterceptor {

    @Autowired
    private LoginStub loginStub;



    /**
     * controller执行前所进行的操作
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        response.setContentType("application/json; charset=UTF-8");
        //判断请求头的token是否存在
        String token = request.getHeader("token");
        if (token != null){
            //判断当前token是否符合redis和jwt的验证
            Result result = loginStub.getUserByToken(token);
            if (result.getCode() == 0){
                //认证完之后进行授权认证,空的时候放行和授权成功的话则放行,有权限校验而且权限不符合的时候进行拦截
                AuthCheck authCheck = ((HandlerMethod) handler).getMethod().getAnnotation(AuthCheck.class);
                if (authCheck == null) {
                    return true;
                }
                String admin = authCheck.user();
                List<String> roleList = (List<String>) result.getData().get("roleList");
                if (roleList != null){
                    if (roleList.contains(admin)){
                        return true;
                    }else {
                        response.getWriter().write("并无当前权限,访问已被拦截");
                        return false;
                    }
                }
            }else {
                String errorMessage = (String) result.getData().get("errorMessage");
                response.getWriter().write(errorMessage);
                return false;
            }
        }
        response.getWriter().write("token无效,请重新登录");
        return false;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    }
}

LoginConfig

/**
 * mvc拦截器
 */
@Configuration
public class LoginConfig implements WebMvcConfigurer {

    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //注册LoginInterceptor拦截器
        InterceptorRegistration registration = registry.addInterceptor(loginInterceptor);
        registration.addPathPatterns("/**");                      //所有路径都被拦截
        registration.excludePathPatterns(                       //添加不拦截路径
                "/druid/*"   //数据库资源
        );
    }
}

LoginStub

调用sso的校验信息接口,获得返回值做二次拦截校验

/**
 * 处理token登录
 */
@Service
public class LoginStub {

    @Autowired
    private RestTemplate restTemplate;

    /**
     * 根据token查询当前用户是否真实
     * @param token
     * @return
     */
    public Result getUserByToken(String token){
        //调用sso中根据token校验用户的接口,后续可做修改
        ResponseEntity<Result> entity = restTemplate.postForEntity("http://localhost:8082/user/getAuthorizationInfo", token, Result.class);
        //获取接口返回的信息
        Result rs=new Result();
        rs.setData(entity.getBody().getData());
        rs.setCode(entity.getBody().getCode());
        return rs;
    }

}

管理端的权限拦截我是用注解的方式来校验的,所以创了个自定义注解来获取所需的权限,再跟用户认证完成后得到的权限做比较。

  • 2
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Boot是一个开源的Java框架,用于构建独立的、可执行的、生产级的Spring应用程序。它极大地简了Spring应用程序的搭建和部署过程,提供了一整套开箱即用的特性和插件,极大地提高了开发效率。 Shiro是一个强大且灵活的开源Java安全框架,提供了身份验证、授权、加密和会话管理等功能,用于保护应用程序的安全。它采用插件的设计,支持与Spring等常用框架的无缝集成,使开发者能够轻松地在应用程序中添加安全功能。 JWT(JSON Web Token)是一种用于在客户端和服务端之间传输安全信息的开放标准。它使用JSON格式对信息进行包装,并使用数字签名进行验证,确保信息的完整性和安全性。JWT具有无状态性、可扩展性和灵活性的特点,适用于多种应用场景,例如身份验证和授权。 Redis是一个开源的、高性能的、支持多种数据结构的内存数据库,同时也可以持久到磁盘中。它主要用于缓存、消息队列、会话管理等场景,为应用程序提供高速、可靠的数据访问服务。Redis支持丰富的数据类型,并提供了强大的操作命令,使开发者能够灵活地处理各种数据需求。 综上所述,Spring Boot结合ShiroJWTRedis可以构建一个安全、高性能的Java应用程序。Shiro提供了强大的安全功能,包括身份验证和授权,保护应用程序的安全;JWT用于安全传输信息,确保信息的完整性和安全性;Redis作为缓存和持久数据库,提供了高速、可靠的数据访问服务。通过使用这些技术,开发者能够快速、高效地构建出符合安全和性能需求的应用程序。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值