zoukankan      html  css  js  c++  java
  • 【SpringBoot技术专题】「权限校验专区」Shiro整合JWT授权和认证实现

    本章介绍一下常用的认证框架Shiro结合springboot以及集合jwt快速带您开发完成一个认证框架机制。

    Maven配置依赖

    <dependency>
    	<groupId>org.apache.shiro</groupId>
    	<artifactId>shiro-spring</artifactId>
    	<version>1.3.2</version>
    </dependency>
    

    Shiro 配置类

    @Configuration
    public class ShiroConfig {
    
        @Bean
        public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
            ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
            // 必须设置 SecurityManager
            shiroFilterFactoryBean.setSecurityManager(securityManager);
            // setLoginUrl 如果不设置值,默认会自动寻找Web工程根目录下的"/login.jsp"页面 或 "/login" 映射
            shiroFilterFactoryBean.setLoginUrl("/notLogin");
            // 设置无权限时跳转的 url;
            shiroFilterFactoryBean.setUnauthorizedUrl("/notRole");
            // 设置拦截器
            Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
            //游客,开发权限
            filterChainDefinitionMap.put("/guest/**", "anon");
            //用户,需要角色权限 “user”
            filterChainDefinitionMap.put("/user/**", "roles[user]");
            //管理员,需要角色权限 “admin”
            filterChainDefinitionMap.put("/admin/**", "roles[admin]");
            //开放登陆接口
            filterChainDefinitionMap.put("/login", "anon");
            //其余接口一律拦截
            //主要这行代码必须放在所有权限设置的最后,不然会导致所有 url 都被拦截
            filterChainDefinitionMap.put("/**", "authc");
            shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
            System.out.println("Shiro拦截器工厂类注入成功");
            return shiroFilterFactoryBean;
        }
    
        /**
         * 注入 securityManager
         */
        @Bean
        public SecurityManager securityManager() {
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            // 设置realm.
            securityManager.setRealm(customRealm());
            return securityManager;
        }
    
        /**
         * 自定义身份认证 realm;
         * <p>
         * 必须写这个类,并加上 @Bean 注解,目的是注入 CustomRealm,
         * 否则会影响 CustomRealm类 中其他类的依赖注入
         */
        @Bean
        public CustomRealm customRealm() {
            return new CustomRealm();
        }
    }
    

    注意

    • SecurityManager类导入的应该是 import org.apache.shiro.mgt.SecurityManager; 但是,如果你是复制代码过来的话,会默认导入 ,java.lang.SecurityManager 这里也稍稍有点坑,其他的类的话,也是都属于shiro 包里面的类。

    • shirFilter 方法中主要是设置了一些重要的跳转 url,比如未登陆时,无权限时的跳转;以及设置了各类 url 的权限拦截,比如 /user 开始的 url 需要 user 权限,/admin 开始的 url 需要 admin 权限等

    权限拦截Filter

    当运行一个Web应用程序时,Shiro将会创建一些有用的默认 Filter 实例,并自动地将它们置为可用,而这些默认的 Filter 实例是被 DefaultFilter 枚举类定义的,当然我们也可以自定义 Filter 实例,这些在以后的文章中会讲到。

    Filter 解释
    anon 无参,开放权限,可以理解为匿名用户或游客
    authc 无参,需要认证
    logout 无参,注销,执行后会直接跳转到shiroFilterFactoryBean.setLoginUrl(); 设置的 url
    authcBasic 无参,表示 httpBasic 认证
    user 无参,表示必须存在用户,当登入操作时不做检查
    ssl 无参,表示安全的URL请求,协议为 https
    perms[user] 参数可写多个,表示需要某个或某些权限才能通过,多个参数时写 perms["user, admin"],当有多个参数时必须每个参数都通过才算通过
    roles[admin] 参数可写多个,表示是某个或某些角色才能通过,多个参数时写 roles["admin,user"],当有多个参数时必须每个参数都通过才算通过
    rest[user] 根据请求的方法,相当于 perms[user:method],其中 method 为 post,get,delete 等
    port[8081] 当请求的URL端口不是8081时,跳转到schemal://serverName:8081?queryString 其中 schmal 是协议 http 或 https 等等,serverName 是你访问的 Host,8081 是 Port 端口,queryString 是你访问的 URL 里的 ? 后面的参数

    常用的主要就是 anon,authc,user,roles,perms 等

    注意

    • 第一组认证过滤器:anon, authc, authcBasic, user
    • 第二组授权过滤器:perms, port, rest, roles, ssl 是,要通过授权过滤器,就先要完成登陆认证操作(即先要完成认证才能前去寻找授权) 才能走第二组授权器(例如访问需要 roles 权限的 url,如果没有登陆,会跳转到 shiroFilterFactoryBean.setLoginUrl(); 设置的 url

    自定义 realm 类

    首先要继承 AuthorizingRealm 类来自定义我们自己的 realm 以进行我们自定义的身份,权限认证操作。

    记得要 Override 重写 doGetAuthenticationInfo 和 doGetAuthorizationInfo 两个方法(两个方法名很相似,不要搞错)

    public class CustomRealm extends AuthorizingRealm {
        private UserMapper userMapper;
    
        @Autowired
        private void setUserMapper(UserMapper userMapper) {
            this.userMapper = userMapper;
        }
    
        /**
         * 获取身份验证信息
         * Shiro中,最终是通过 Realm 来获取应用程序中的用户、角色及权限信息的。
         * @param authenticationToken 用户身份信息 token
         * @return 返回封装了用户信息的 AuthenticationInfo 实例
         */
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
            UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
            // 从数据库获取对应用户名密码的用户
            String password = userMapper.getPassword(token.getUsername());
            if (null == password) {
                throw new AccountException("用户名不正确");
            } else if (!password.equals(new String((char[]) token.getCredentials()))) {
                throw new AccountException("密码不正确");
            }
            return new SimpleAuthenticationInfo(token.getPrincipal(), password, getName());
        }
    
        /**
         * 获取授权信息
         *
         * @param principalCollection
         * @return
         */
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
            System.out.println("————权限认证————");
            String username = (String) SecurityUtils.getSubject().getPrincipal();
            SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
            //获得该用户角色
            String role = userMapper.getRole(username);
            Set<String> set = new HashSet<>();
            //需要将 role 封装到 Set 作为 info.setRoles() 的参数
            set.add(role);
            //设置该用户拥有的角色
            info.setRoles(set);
            return info;
        }
    }
    
    • 重写的两个方法分别是实现身份认证以及权限认证,shiro 中有个作登陆操作的Subject.login() 方法,当我们把封装了用户名,密码的 token 作为参数传入,便会跑进这两个方法里面。

      • doGetAuthorizationInfo 方法只有在需要权限认证时才会进去,比如前面配置类中配置了 filterChainDefinitionMap.put("/admin/**", "roles[admin]"); 的管理员角色,这时进入 /admin 时就会进入 doGetAuthorizationInfo 方法来检查权限;

      • doGetAuthenticationInfo 方法则是需要身份认证时(比如前面的Subject.login() 方法)才会进入

      • UsernamePasswordToken类,我们可以从该对象拿到登陆时的用户名和密码(登陆时会使用 new UsernamePasswordToken(username, password);),而 get 用户名或密码有以下几个方法

    token.getUsername()  //获得用户名 String
    token.getPrincipal() //获得用户名 Object 
    token.getPassword()  //获得密码 char[]
    token.getCredentials() //获得密码 Object
    

    注意

    有很多人会发现,UserMapper 等类,接口无法通过 @Autowired 注入进来,跑程序的时候会报 NullPointerException,网上说了很多诸如是 Spring 加载顺序等原因,但其实有一个很重要的地方要大家注意,CustomRealm 这个类是在 shiro 配置类的 securityManager.setRealm() 方法中设置进去的,而很多人直接写securityManager.setRealm(new CustomRealm());,这样是不行的,必须要使用 @Bean注入MyRealm,不能直接 new 对象:

        @Bean
        public SecurityManager securityManager() {
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            // 设置realm.
            securityManager.setRealm(customRealm());
            return securityManager;
        }
        @Bean
        public CustomRealm customRealm() {
            return new CustomRealm();
        }
      @Bean
        public SecurityManager securityManager(CustomRealm customRealm) {
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            // 设置realm.
            securityManager.setRealm(customRealm);
            return securityManager;
        }
    

    然后只要在 CustomRealm 类加上个类似 @Component 的注解即可

    功能实现

    本文的功能全部以接口返回 json 数据的方式实现

    根据 url 权限分配 controller

    游客

    @RestController
    @RequestMapping("/guest")
    public class GuestController{
        @Autowired
        private final ResultMap resultMap;
    
        @RequestMapping(value = "/enter", method = RequestMethod.GET)
        public ResultMap login() {
            return resultMap.success().message("欢迎进入,您的身份是游客");
        }
    
        @RequestMapping(value = "/getMessage", method = RequestMethod.GET)
        public ResultMap submitLogin() {
            return resultMap.success().message("您拥有获得该接口的信息的权限!");
        }
    }
    

    普通登陆用户

    @RestController
    @RequestMapping("/user")
    public class UserController{
        @Autowired
        private final ResultMap resultMap;
        @RequestMapping(value = "/getMessage", method = RequestMethod.GET)
        public ResultMap getMessage() {
            return resultMap.success().message("您拥有用户权限,可以获得该接口的信息!");
        }
    }
    

    管理员

    @RestController
    @RequestMapping("/admin")
    public class AdminController {
        @Autowired
        private final ResultMap resultMap;
        @RequestMapping(value = "/getMessage", method = RequestMethod.GET)
        public ResultMap getMessage() {
            return resultMap.success().message("您拥有管理员权限,可以获得该接口的信息!");
        }
    }
    

    突然注意到 CustomRealm 类那里抛出了 AccountException 异常,现在建个类进行异常捕获

    @RestControllerAdvice
    public class ExceptionController {
        private final ResultMap resultMap;
        @Autowired
        public ExceptionController(ResultMap resultMap) {
            this.resultMap = resultMap;
        }
        // 捕捉 CustomRealm 抛出的异常
        @ExceptionHandler(AccountException.class)
        public ResultMap handleShiroException(Exception ex) {
            return resultMap.fail().message(ex.getMessage());
        }
    }
    

    还有进行登陆等处理的 LoginController

    @RestController
    public class LoginController {
        @Autowired
        private ResultMap resultMap;
        private UserMapper userMapper;
    
        @RequestMapping(value = "/notLogin", method = RequestMethod.GET)
        public ResultMap notLogin() {
            return resultMap.success().message("您尚未登陆!");
        }
    
        @RequestMapping(value = "/notRole", method = RequestMethod.GET)
        public ResultMap notRole() {
            return resultMap.success().message("您没有权限!");
        }
    
        @RequestMapping(value = "/logout", method = RequestMethod.GET)
        public ResultMap logout() {
            Subject subject = SecurityUtils.getSubject();
            //注销
            subject.logout();
            return resultMap.success().message("成功注销!");
        }
    
        /**
         * 登陆
         * @param username 用户名
         * @param password 密码
         */
        @RequestMapping(value = "/login", method = RequestMethod.POST)
        public ResultMap login(String username, String password) {
            // 从SecurityUtils里边创建一个 subject
            Subject subject = SecurityUtils.getSubject();
            // 在认证提交前准备 token(令牌)
            UsernamePasswordToken token = new UsernamePasswordToken(username, password);
            // 执行认证登陆
            subject.login(token);
            //根据权限,指定返回数据
            String role = userMapper.getRole(username);
            if ("user".equals(role)) {
                return resultMap.success().message("欢迎登陆");
            }
            if ("admin".equals(role)) {
                return resultMap.success().message("欢迎来到管理员页面");
            } 
            return resultMap.fail().message("权限错误!");
        }
    }
    

    Jwt整合shiro服务框架

    JWTUtil

    利用 JWT 的工具类来生成我们的 token,这个工具类主要有生成 token 和 校验 token 两个方法

    生成 token 时,指定 token 过期时间 EXPIRE_TIME 和签名密钥 SECRET,然后将 date 和 username 写入 token 中,并使用带有密钥的 HS256 签名算法进行签名

    Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
    Algorithm algorithm = Algorithm.HMAC256(SECRET);
    JWT.create()
        .withClaim("username", username)
        //到期时间
        .withExpiresAt(date)
        //创建一个新的JWT,并使用给定的算法进行标记
        .sign(algorithm);
    

    过滤器

    因为 JWT 的整合,我们需要自定义自己的过滤器 JWTFilter,JWTFilter 继承了 BasicHttpAuthenticationFilter,并部分原方法进行了重写

    该过滤器主要有三步:''

    1. 检验请求头是否带有 token ((HttpServletRequest) request).getHeader("Token") != null
    2. 如果带有 token,执行 shiro 的 login() 方法,将 token 提交到 Realm 中进行检验;如果没有 token,说明当前状态为游客状态(或者其他一些不需要进行认证的接口)
        @Override
        protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws UnauthorizedException {
            //判断请求的请求头是否带上 "Token"
            if (((HttpServletRequest) request).getHeader("Token") != null) {
                //如果存在,则进入 executeLogin 方法执行登入,检查 token 是否正确
                try {
                    executeLogin(request, response);
                    return true;
                } catch (Exception e) {
                    //token 错误
                    responseError(response, e.getMessage());
                }
            }
            //如果请求头不存在 Token,则可能是执行登陆操作或者是游客状态访问,无需检查 token,直接返回 true
            return true;
        }
    
        @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;
        }
    
    1. 如果在 token 校验的过程中出现错误,如 token 校验失败,那么我会将该请求视为认证不通过,则重定向到 /unauthorized/**

    另外,我将跨域支持放到了该过滤器来处理

    Realm 类

    依然是我们的自定义 Realm ,对这一块还不了解的可以先看我的上一篇 shiro 的文章

    • 身份认证
    if (username == null || !JWTUtil.verify(token, username)) {
        throw new AuthenticationException("token认证失败!");
    }
    String password = userMapper.getPassword(username);
    if (password == null) {
        throw new AuthenticationException("该用户不存在!");
    }
    int ban = userMapper.checkUserBanStatus(username);
    if (ban == 1) {
        throw new AuthenticationException("该用户已被封号!");
    }
    

    拿到传来的 token ,检查 token 是否有效,用户是否存在,以及用户的封号情况

    • 权限认证
    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    //获得该用户角色
    String role = userMapper.getRole(username);
    //每个角色拥有默认的权限
    String rolePermission = userMapper.getRolePermission(username);
    //每个用户可以设置新的权限
    String permission = userMapper.getPermission(username);
    Set<String> roleSet = new HashSet<>();
    Set<String> permissionSet = new HashSet<>();
    //需要将 role, permission 封装到 Set 作为 info.setRoles(), info.setStringPermissions() 的参数
    roleSet.add(role);
    permissionSet.add(rolePermission);
    permissionSet.add(permission);
    //设置该用户拥有的角色和权限
    info.setRoles(roleSet);
    info.setStringPermissions(permissionSet);
    

    利用 token 中获得的 username,分别从数据库查到该用户所拥有的角色,权限,存入 SimpleAuthorizationInfo 中

    ShiroConfig 配置类

    设置好我们自定义的 filter,并使所有请求通过我们的过滤器,除了我们用于处理未认证请求的 /unauthorized/**

    @Bean
    public ShiroFilterFactoryBean factory(SecurityManager securityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        // 添加自己的过滤器并且取名为jwt
        Map<String, Filter> filterMap = new HashMap<>();
        //设置我们自定义的JWT过滤器
        filterMap.put("jwt", new JWTFilter());
        factoryBean.setFilters(filterMap);
        factoryBean.setSecurityManager(securityManager);
        Map<String, String> filterRuleMap = new HashMap<>();
        // 所有请求通过我们自己的JWT Filter
        filterRuleMap.put("/**", "jwt");
        // 访问 /unauthorized/** 不通过JWTFilter
        filterRuleMap.put("/unauthorized/**", "anon");
        factoryBean.setFilterChainDefinitionMap(filterRuleMap);
        return factoryBean;
    }
    

    权限控制注解 @RequiresRoles, @RequiresPermissions

    这两个注解为我们主要的权限控制注解, 如

    // 拥有 admin 角色可以访问
    @RequiresRoles("admin")
    
    // 拥有 user 或 admin 角色可以访问
    @RequiresRoles(logical = Logical.OR, value = {"user", "admin"})
    
    // 拥有 vip 和 normal 权限可以访问
    @RequiresPermissions(logical = Logical.AND, value = {"vip", "normal"})
    
    // 拥有 user 或 admin 角色,且拥有 vip 权限可以访问
    @GetMapping("/getVipMessage")
    @RequiresRoles(logical = Logical.OR, value = {"user", "admin"})
    @RequiresPermissions("vip")
    public ResultMap getVipMessage() {
        return resultMap.success().code(200).message("成功获得 vip 信息!");
    }
    

    当我们写的接口拥有以上的注解时,如果请求没有带有 token 或者带了 token 但权限认证不通过,则会报 UnauthenticatedException 异常,但是我在 ExceptionController 类对这些异常进行了集中处理

    @ExceptionHandler(ShiroException.class)
    public ResultMap handle401() {
        return resultMap.fail().code(401).message("您没有权限访问!");
    }
    

    这时,出现 shiro 相关的异常时则会返回

    {
        "result": "fail",
        "code": 401,
        "message": "您没有权限访问!"
    }
    

    除了以上两种,还有 @RequiresAuthentication ,@RequiresUser 等注解

    功能实现

    用户角色分为三类,管理员 admin,普通用户 user,游客 guest;admin 默认权限为 vip,user 默认权限为 normal,当 user 升级为 vip 权限时可以访问 vip 权限的页面。

    登陆

    登陆接口不带有 token,当登陆密码,用户名验证正确后返回 token。

    @PostMapping("/login")
    public ResultMap login(@RequestParam("username") String username,
                           @RequestParam("password") String password) {
        String realPassword = userMapper.getPassword(username);
        if (realPassword == null) {
            return resultMap.fail().code(401).message("用户名错误");
        } else if (!realPassword.equals(password)) {
            return resultMap.fail().code(401).message("密码错误");
        } else {
            return resultMap.success().code(200).message(JWTUtil.createToken(username));
        }
    }
    
    {
        "result": "success",
        "code": 200,
        "message": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1MjUxODQyMzUsInVzZXJuYW1lIjoiaG93aWUifQ.fG5Qs739Hxy_JjTdSIx_iiwaBD43aKFQMchx9fjaCRo"
    }
    

    异常处理

        // 捕捉shiro的异常
        @ExceptionHandler(ShiroException.class)
        public ResultMap handle401() {
            return resultMap.fail().code(401).message("您没有权限访问!");
        }
    
        // 捕捉其他所有异常
        @ExceptionHandler(Exception.class)
        public ResultMap globalException(HttpServletRequest request, Throwable ex) {
            return resultMap.fail()
                    .code(getStatus(request).value())
                    .message("访问出错,无法访问: " + ex.getMessage());
        }
    

    权限控制

    • UserController(user 或 admin 可以访问)

    在接口上带上 @RequiresRoles(logical = Logical.OR, value = {"user", "admin"})

    • vip 权限
      再加上@RequiresPermissions("vip")

    • AdminController(admin 可以访问)
      在接口上带上 @RequiresRoles("admin")

    • GuestController(所有人可以访问)
      不做权限处理

    极限就是为了超越而存在的
  • 相关阅读:
    【Educational Codeforces Round 101 (Rated for Div. 2) C】Building a Fence
    【Codeforces Round #698 (Div. 2) C】Nezzar and Symmetric Array
    【Codeforces Round #696 (Div. 2) D】Cleaning
    【Codeforces Round #696 (Div. 2) C】Array Destruction
    【Educational Codeforces Round 102 D】Program
    【Educational Codeforces Round 102 C】No More Inversions
    【Good Bye 2020 G】Song of the Sirens
    【Good Bye 2020 F】Euclid's nightmare
    使用mobx入门
    requestAnimationFrame 控制速度模拟setinterval
  • 原文地址:https://www.cnblogs.com/liboware/p/15360022.html
Copyright © 2011-2022 走看看