• 项目实战(依旧还是登录认证,JWT解析异常处理,授权信息处理)


    54. 关于SecurityContext中的认证信息

    Spring Security框架是根据SecurityContext中是否存在认证信息来判断用户是否已经登录。

    关于SecurityContext,是通过ThreadLocal进行处理的,所以,是线程安全的,每个客户端对应的SecurityContext中的信息是互不干扰的。

    另外,SecurityContext中的认证信息是通过Session存储的,所以,一旦向SecurityContext中存入了认证信息,在后续一段时间(Session的有效时间)的访问中,即使不携带JWT,也是允许访问的,会被视为“已登录”。如果认为这样的表现是不安全的,可以在JWT过滤器中,在刚刚接收到请求时,就直接清除SecurityContext中的信息(主要是认证信息):

    // 清除SecurityContext中原有的数据(认证信息)
    SecurityContextHolder.clearContext();
    
    • 1
    • 2

    53. 自定义配置

    在处理JWT时,无论是生成JWT,还是解析JWT,都需要使用同一个secretKey,则应该将此secretKey定义在某个类中作为静态常量,或定义在配置文件(application.yml或等效的配置文件)中,由于此值是允许被软件的使用者(甲方)自行定义的,所以,更推荐定义在配置文件中。

    则在application-dev.yml中添加自定义配置:

    # 自定义配置
    csmall:
      jwt:
        secret-key: kns439a}fdLK34jsmfd{MF5-8DJSsLKhJNFDSjn
    
    • 1
    • 2
    • 3
    • 4

    提示:在配置文件中的自定义属性,应该在属性名称上添加统一的、自定义的前缀,例如以上使用到的csmall,以便于与其它的属性区分开来。

    接下来,可以在需要使用以上配置值的类中,通过@Value注解将以上配置值注入到某个全局属性中,例如:

    @Value("${csmall.jwt.secret-key}")
    String secretKey;
    
    • 1
    • 2

    提示:以上使用的@Value注解可以读取当前项目中的全部环境变量,将包括:操作系统的环境变量、JVM的环境变量、各配置文件中的配置。并且,@Value注解可以添加在全局属性上,也可以添加在被Spring自动调用的方法的参数上。

    54. 处理解析JWT时的异常

    在JWT过滤器中,解析JWT时可能会出现异常,异常的类型主要有:

    • SignatureException
    • MalformedJwtException
    • ExpiredJwtException

    由于解析JWT是发生成过滤器中的,而过滤器是整个Java EE体系中最早接收到请求的组件(此时,控制器等其它组件均未开始执行),所以,此时出现的异常不可以使用Spring MVC的全局异常处理器进行处理。

    提示:Spring MVC的全局异常处理器在控制器(Controller)抛出异常之后执行。

    只能通过最原始的try...catch...语法捕获并处理异常,处理时,需要使用到过滤器方法的第2个参数HttpServletResponse response来向客户端响应错误信息。

    为了便于封装错误信息,应该使用JsonResult来封装相关信息,由于需要自行将JsonResult格式的对象转换成JSON格式的数据,所以,需要在pom.xml添加能够实现对象与JSON格式字符串相互转换的依赖,例如可以添加fastjson依赖:

    
    <dependency>
        <groupId>com.alibabagroupId>
        <artifactId>fastjsonartifactId>
        <version>1.2.75version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    然后,在ServiceCode中添加一些新的业务状态码:

    public enum ServiceCode {
    
        // 前序代码
    
        ERR_JWT_SIGNATURE(60000),
        ERR_JWT_MALFORMED(60000),
        ERR_JWT_EXPIRED(60002),
        ERR_UNKNOWN(99999);
    
        // 后续代码
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    再开始处理异常,例如:

    // 尝试解析JWT
    log.debug("将尝试解析JWT……");
    Claims claims = null;
    try {
        claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();
    } catch (SignatureException e) {
        String message = "非法访问!";
        JsonResult jsonResult = JsonResult.fail(ServiceCode.ERR_JWT_SIGNATURE, message);
        String jsonResultString = JSON.toJSONString(jsonResult);
        PrintWriter writer = response.getWriter();
        writer.println(jsonResultString);
        writer.close();
        return;
    } catch (MalformedJwtException e) {
        String message = "非法访问!";
        JsonResult jsonResult = JsonResult.fail(ServiceCode.ERR_JWT_MALFORMED, message);
        String jsonResultString = JSON.toJSONString(jsonResult);
        PrintWriter writer = response.getWriter();
        writer.println(jsonResultString);
        writer.close();
        return;
    } catch (ExpiredJwtException e) {
        String message = "登录已过期,请重新登录!";
        JsonResult jsonResult = JsonResult.fail(ServiceCode.ERR_JWT_EXPIRED, message);
        String jsonResultString = JSON.toJSONString(jsonResult);
        PrintWriter writer = response.getWriter();
        writer.println(jsonResultString);
        writer.close();
        return;
    } catch (Throwable e) {
        e.printStackTrace(); // 重要
        String message = "服务器忙,请稍后再次尝试!";
        JsonResult jsonResult = JsonResult.fail(ServiceCode.ERR_UNKNOWN, message);
        String jsonResultString = JSON.toJSONString(jsonResult);
        PrintWriter writer = response.getWriter();
        writer.println(jsonResultString);
        writer.close();
        return;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    注意:强烈推荐在最后补充处理Throwable异常,以避免某些异常未被考虑到,并且,在处理Throwable时,应该执行e.printStackTrace(),则出现未预测的异常时,可以通过控制台看到相关信息,并在后续补充对这些异常的精准处理!

    55. 处理授权

    首先,需要调整现有的AdminMapper接口中的AdminLoginInfoVO getLoginInfoByUsername(String username)方法,此方法应该返回参数用户名匹配的管理员信息,信息中应该包含权限!

    则需要执行的SQL语句大致是:

    SELECT
        ams_admin.id,
        ams_admin.username,
        ams_admin.password,
        ams_admin.enable,
        ams_permission.value
    FROM
        ams_admin
    LEFT JOIN ams_admin_role ON ams_admin.id=ams_admin_role.admin_id
    LEFT JOIN ams_role_permission ON ams_admin_role.role_id=ams_role_permission.role_id
    LEFT JOIN ams_permission ON ams_role_permission.permission_id=ams_permission.id
    WHERE
        username='root';
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    则需要调整AdminLoginInfoVO类,添加新的属性,用于封装查询到的权限信息:

    private List<String> permissions;
    
    • 1

    然后调整AdminMapper.xml中的相关配置:

    
    <select id="getLoginInfoByUsername" resultMap="LoginResultMap">
        SELECT
            <include refid="LoginQueryFields"/>
        FROM
            ams_admin
        LEFT JOIN ams_admin_role ON ams_admin.id=ams_admin_role.admin_id
        LEFT JOIN ams_role_permission ON ams_admin_role.role_id=ams_role_permission.role_id
        LEFT JOIN ams_permission ON ams_role_permission.permission_id=ams_permission.id
        WHERE
            username=#{username}
    select>
    
    <sql id="LoginQueryFields">
        <if test="true">
            ams_admin.id,
            ams_admin.username,
            ams_admin.password,
            ams_admin.enable,
            ams_permission.value
        if>
    sql>
    
    
    
    
    
    
    
    <resultMap id="LoginResultMap" type="cn.tedu.csmall.passport.pojo.vo.AdminLoginInfoVO">
        <id column="id" property="id"/>
        <result column="username" property="username"/>
        <result column="password" property="password"/>
        <result column="enable" property="enable"/>
        <collection property="permissions" ofType="java.lang.String">
            <constructor>
                <arg column="value"/>
            constructor>
        collection>
    resultMap>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    完成后,可以通过AdminMapperTests中原有的测试方法直接测试,测试结果例如:

    根据username=fanchuanqi查询登录信息完成,结果=AdminLoginInfoVO(id=5, username=fanchuanqi, password=$2a$10$N.ZOn9G6/YLFixAOPMg/h.z7pCu6v2XyFDtC4q.jeeGm/TEZyj15C, enable=0, permissions=[/pms/picture/read, /pms/picture/add-new, /pms/picture/delete, /pms/picture/update, /pms/album/read, /pms/album/add-new, /pms/album/delete, /pms/album/update])
    
    • 1

    接下来,在UserDetailsServiceImpl中,向返回的AdminDetails中封装真实的权限数据:

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        log.debug("Spring Security调用了loadUserByUsername()方法,参数:{}", s);
        AdminLoginInfoVO loginInfo = adminMapper.getLoginInfoByUsername(s);
        log.debug("从数据库查询与用户名【{}】匹配的管理员信息:{}", s, loginInfo);
    
        if (loginInfo == null) {
            log.debug("此用户名【{}】不存在,即将抛出异常");
            String message = "登录失败,用户名不存在!";
            throw new BadCredentialsException(message);
        }
    
        // ===== 以下是此次调整的内容 =====
        List<GrantedAuthority> authorities = new ArrayList<>();
        for (String permission : loginInfo.getPermissions()) {
            GrantedAuthority authority = new SimpleGrantedAuthority(permission);
            authorities.add(authority);
        }
    
        AdminDetails adminDetails = new AdminDetails(
                loginInfo.getUsername(), loginInfo.getPassword(),
                loginInfo.getEnable() == 1, authorities);
        adminDetails.setId(loginInfo.getId());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    经过以上调整后,在AdminServiceImpl处理登录的login()方法中,认证返回的结果的当事人(Principal)中就包含管理员的权限信息了!

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mVG0hBTq-1667124989429)(file://C:\Users\lenovo\Desktop\第四阶段\doc\note\images\DAY16\image-20221014163109305.png)]

    AdminServiceImpl类中的login()方法中,当认证成功后,得到的认证信息中的当事人信息就包含以上返回的AdminDetails,也就包含了管理员的权限信息,需要将此权限信息转换为JSON字符串(如果不转换,则后续解析时不便于还原出原始数据)并用于生成JWT数据:

    log.debug("准备生成JWT数据");
    Map<String, Object> claims = new HashMap<>();
    claims.put("id", adminDetails.getId()); // 向JWT中封装id
    claims.put("username", adminDetails.getUsername()); // 向JWT中封装username
    // ===== 以下这1条语句是新增的 =====
    claims.put("authorities", JSON.toJSONString(adminDetails.getAuthorities())); // 向JWT中封装权限
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在JWT过滤器中,在解析JWT时,可以从JWT中得到权限的JSON字符串,应该将其转换成List并存入到认证信息中:

    // 从JWT中获取用户的相关数据,例如id、username等
    Long id = claims.get("id", Long.class);
    String username = claims.get("username", String.class);
    String authoritiesJsonString = claims.get("authorities", String.class); // 【调整】
    log.debug("从JWT中解析得到数据:id={}", id);
    log.debug("从JWT中解析得到数据:username={}", username);
    log.debug("从JWT中解析得到数据:authoritiesJsonString={}", authoritiesJsonString);
    
    // 准备用于创建认证信息的权限数据
    List<SimpleGrantedAuthority> authorities
            = JSON.parseArray(authoritiesJsonString, SimpleGrantedAuthority.class); // 【调整】
    
    // 准备用于创建认证信息的当事人数据
    LoginPrincipal loginPrincipal = new LoginPrincipal();
    loginPrincipal.setId(id);
    loginPrincipal.setUsername(username);
    
    // 创建认证信息
    Authentication authentication = new UsernamePasswordAuthenticationToken(
            loginPrincipal, null, authorities);
    
    // 将认证信息存储到SecurityContext中
    log.debug("即将向SecurityContext中存入认证信息:{}", authentication);
    SecurityContextHolder.getContext().setAuthentication(authentication);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    至此,关于登录相关的处理已经全部结束!

    接下来,就可以配置各请求所需的权限,以实现对管理员权限的控制!

    需要先在配置类上添加@EnableGlobalMethodSecurity(prePostEnabled = true)开启在方法上使用注解配置权限的功能,则在SecurityConfiguration类中添加此注解配置:

    @Slf4j
    @Configuration
    @EnableGlobalMethodSecurity(prePostEnabled = true) // 新增
    public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
        // 暂不关心类中的代码
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    然后,在任何处理请求的方法上,通过@PreAuthorize注解来配置对应请求所需的权限,例如:

    @ApiOperation("查询管理员列表")
    @ApiOperationSupport(order = 420)
    @GetMapping("")
    @PreAuthorize("hasAuthority('/ams/admin/read')") // 【新增】
    public JsonResult<List<AdminListItemVO>> list() {
        log.debug("开始处理【查询管理员列表】的请求");
        List<AdminListItemVO> list = adminService.list();
        return JsonResult.ok(list);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    经过以上配置,则“查询管理员列表”功能是只有具备/ams/admin/read权限的管理员才允许访问的,如果当前JWT对应的管理员不具备此权限,则会出现AccessDeniedException,例如:

    org.springframework.security.access.AccessDeniedException: 不允许访问
    
    • 1
  • 相关阅读:
    大数据企业申报!2022年度武汉市大数据企业认定条件、流程和材料汇编
    【每天学习一点新知识】网络安全--拒绝服务攻击
    CF566E-Restoring Map【bitset】
    你是否感受到AI就在身边?
    BUU [CISCN2019 华东南赛区]Web4
    半监督支持向量机
    (附源码)spring boot学业指导系统 毕业设计 030958
    idea如何快速找到项目中对应的类(包括源码)
    Era Network在web3行业的价值
    如何在Win系统部署Tomcat服务并实现远程访问内网站点
  • 原文地址:https://blog.csdn.net/weixin_43121885/article/details/127602750