• 项目实战(JWT登录,认证信息中的当事人信息,跨域请求和预检信息)


    50. 关于JWT(续)

    JWT是不安全的,因为在不知道secretKey的情况下,任何JWT都是可以解析出Header、Payload部分的,这2部分的数据并没有做任何加密处理,所以,如果JWT数据被暴露,则任何人都可以从中解析出Header、Payload中的数据!

    至于JWT中的secretKey,及生成JWT时使用的算法,是用于对Header、Payload执行签名算法的,JWT中的Signature是用于验证JWT真伪的。

    当然,如果你认为有必要的话,可以自行另外使用加密算法,将Payload中应该封装的数据先加密,再用于生成JWT!

    另外,如果JWT数据被泄露,他人使用有效的JWT是可以正常使用的!所以,通常,在相对比较封闭的操作系统(例如智能手机的操作系统)中,JWT的有效时间可以设置得很长,但是,不太封闭的操作系统(例如PC端的操作系统)中,JWT的有效时间应该相对较短。

    所以,在JWT时,需要注意:

    • 根据你所需的安全性,来设置JWT的有效时间
    • 不要在JWT中存放敏感数据,例如:手机号码、身份证号码、明文密码
    • 如果一定要在JWT中存放敏感数据,应该自行使用加密算法处理过后再用于生成JWT

    51. 登录成功时生成并响应JWT

    在使用JWT的项目,用户登录就相当于现实生活乘车之前购买火车票的过程,所以,当用户登录成功时,需要生成对应的JWT数据,并响应到客户端。

    首先,需要修改IAdminService接口中处理登录的抽象方法的声明,将返回值类型改为String,表示将返回成功登录的JWT数据:

    /**
     * 管理员登录
     *
     * @param adminLoginDTO 封装了管理员的登录信息的对象
     * @return 成功登录的JWT数据
     */
    String login(AdminLoginDTO adminLoginDTO);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    然后,在AdminServiceImpl实现类中,也修改重写的方法的声明,并且,在登录成功后,生成、返回JWT数据:

    log.debug("准备生成JWT数据");
    Map<String, Object> claims = new HashMap<>();
    // claims.put("id", null); // 向JWT中封装id
    claims.put("username", adminLoginDTO.getUsername()); // 向JWT中封装username
    
    String secretKey = "kns439a}fdLK34jsmfd{MF5-8DJSsLKhJNFDSjn";
    Date expirationDate = new Date(System.currentTimeMillis() + 10 * 24 * 60 * 60 * 1000);
    String jwt = Jwts.builder()
            .setHeaderParam("alg", "HS256")
            .setHeaderParam("typ", "JWT")
            .setClaims(claims)
            .setExpiration(expirationDate)
            .signWith(SignatureAlgorithm.HS256, secretKey)
            .compact();
    log.debug("返回JWT数据:{}", jwt);
    return jwt;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    提示:以上代码并不是最终版本。

    最后,在AdminController中,还需要响应JWT数据:

    // http://localhost:9081/admins/login
    @PostMapping("/login")
    public JsonResult<String> login(AdminLoginDTO adminLoginDTO) {
        log.debug("开始处理【管理员登录】的请求,参数:{}", adminLoginDTO);
        String jwt = adminService.login(adminLoginDTO);
        return JsonResult.ok(jwt);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    完成后,重启项目,可以在API文档中测试访问,当登录成功后,响应的结果大致是:

    {
      "state": 20000,
      "data": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NjY0ODk1NjMsInVzZXJuYW1lIjoic3VwZXJfYWRtaW4ifQ.T5wnIVFk-AhvxPETloDsSgx46vdV45Y3BRk1_0oc3CM"
    }
    
    • 1
    • 2
    • 3
    • 4

    关于处理认证的细节

    当调用了AuthenticationManager对象的authenticate()方法,且通过认证后,此方法将返回Authentication接口类型的对象,此对象的具体类型是UsernamePasswordAuthenticationToken,此对象中包含名为Principal(当事人)的属性,值为UserDetailsService对象中loadUserByUsername()返回的对象!

    另外,目前在UserDetailsServiceImpl中返回的UserDetails接口类型的对象是User类型的,此类型没有id属性,如果需要向JWT中封装id甚至其它属性,必须自定义类,继承自User或实现UserDetails接口,在自定义类中补充声明所需的属性,并在UserDetailsServiceImpl中返回自定义类的对象,则处理认证通过后,返回的Authentication中的Principal就是自定义类的对象!

    security包中创建AdminDetails类,继承自User对其进行扩展:

    @Setter
    @Getter
    @EqualsAndHashCode
    @ToString(callSuper = true)
    public class AdminDetails extends User {
    
        private Long id;
    
        public AdminDetails(String username, String password, boolean enabled,
                            Collection<? extends GrantedAuthority> authorities) {
            super(username, password, enabled,
                    true, true, true,
                    authorities);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    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<>();
        GrantedAuthority authority = new SimpleGrantedAuthority("这是一个山寨的权限标识");
        authorities.add(authority);
    
        AdminDetails adminDetails = new AdminDetails(
                loginInfo.getUsername(), loginInfo.getPassword(),
                loginInfo.getEnable() == 1, authorities);
        adminDetails.setId(loginInfo.getId());
    
        log.debug("即将向Spring Security返回UserDetails接口类型的对象:{}", adminDetails);
        return adminDetails;
    }
    
    • 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

    经过以上调整,当AuthenticationManager执行authenticate()认证方法后,如果登录成功,返回的Authentication中的Principal就是以上返回的AdminDetails对象,则可以从中获取idusername等数据,用于生成JWT数据,则在AdminServiceImpl中的login()方法中:

    @Override
    public String login(AdminLoginDTO adminLoginDTO) {
        log.debug("开始处理【管理员登录】的业务,参数:{}", adminLoginDTO);
        // 调用AuthenticationManager对象的authenticate()方法处理认证
        Authentication authentication
                = new UsernamePasswordAuthenticationToken(
                        adminLoginDTO.getUsername(), adminLoginDTO.getPassword());
        Authentication authenticateResult
                = authenticationManager.authenticate(authentication);
        log.debug("执行认证成功,AuthenticationManager返回:{}", authenticateResult);
        Object principal = authenticateResult.getPrincipal();
        log.debug("认证结果中的Principal数据类型:{}", principal.getClass().getName());
        log.debug("认证结果中的Principal数据:{}", principal);
        AdminDetails adminDetails = (AdminDetails) principal;
    
        log.debug("准备生成JWT数据");
        Map<String, Object> claims = new HashMap<>();
        claims.put("id", adminDetails.getId()); // 向JWT中封装id
        claims.put("username", adminDetails.getUsername()); // 向JWT中封装username
    
        String secretKey = "kns439a}fdLK34jsmfd{MF5-8DJSsLKhJNFDSjn";
        Date expirationDate = new Date(System.currentTimeMillis() + 10 * 24 * 60 * 60 * 1000);
        String jwt = Jwts.builder()
                .setHeaderParam("alg", "HS256")
                .setHeaderParam("typ", "JWT")
                .setClaims(claims)
                .setExpiration(expirationDate)
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .compact();
        log.debug("返回JWT数据:{}", jwt);
        return jwt;
    }
    
    • 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

    至此,当客户端向服务器端提交登录请求,且登录成功后,将得到服务器端响应的JWT数据,此JWT中包含了idusername

    解析JWT

    当客户端已经登录成功并得到JWT,相当于现实生活中某人已经成功购买到了火车票,接下来,此人应该携带火车票去乘车,在程序中,就表现为:客户端应该携带JWT向服务器端提交请求。

    关于客户端携带JWT数据,业内惯用的做法是客户端应该将JWT放在请求头(Request Headers)中名为Authorization的属性中。

    在服务器端,通常使用过滤器组件来解析JWT数据。

    在项目的根包下创建JwtAuthorizationFilter

    package cn.tedu.csmall.passport.filter;
    
    import io.jsonwebtoken.Claims;
    import io.jsonwebtoken.Jws;
    import io.jsonwebtoken.Jwts;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.core.context.SecurityContext;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.stereotype.Component;
    import org.springframework.util.StringUtils;
    import org.springframework.web.filter.OncePerRequestFilter;
    
    import javax.servlet.FilterChain;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * JWT认证过滤器
     *
     * 

    Spring Security框架会自动从SecurityContext读取认证信息,如果存在有效信息,则视为已登录,否则,视为未登录

    *

    当前过滤器应该尝试解析客户端可能携带的JWT,如果解析成功,则创建对应的认证信息,并存储到SecurityContext中

    * * @author java@tedu.cn * @version 0.0.1 */
    @Slf4j @Component public class JwtAuthorizationFilter extends OncePerRequestFilter { public static final int JWT_MIN_LENGTH = 100; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 尝试获取客户端提交请求时可能携带的JWT String jwt = request.getHeader("Authorization"); log.debug("接收到JWT数据:{}", jwt); // 判断是否获取到有效的JWT if (!StringUtils.hasText(jwt) || jwt.length() < JWT_MIN_LENGTH) { // 直接放行 log.debug("未获取到有效的JWT数据,将直接放行"); filterChain.doFilter(request, response); return; } // 尝试解析JWT,从中获取用户的相关数据,例如id、username等 log.debug("将尝试解析JWT……"); String secretKey = "kns439a}fdLK34jsmfd{MF5-8DJSsLKhJNFDSjn"; Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody(); Long id = claims.get("id", Long.class); String username = claims.get("username", String.class); log.debug("从JWT中解析得到数据:id={}", id); log.debug("从JWT中解析得到数据:username={}", username); // 将根据从JWT中解析得到的数据来创建认证信息 List<GrantedAuthority> authorities = new ArrayList<>(); GrantedAuthority authority = new SimpleGrantedAuthority("这是一个山寨的权限标识"); authorities.add(authority); Authentication authentication = new UsernamePasswordAuthenticationToken( username, null, authorities); // 将认证信息存储到SecurityContext中 SecurityContext securityContext = SecurityContextHolder.getContext(); securityContext.setAuthentication(authentication); // 放行 filterChain.doFilter(request, response); } }
    • 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
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80

    完成后,还需要在SecurityConfiguration中自动装配自定义的JWT过滤器:

    @Autowired
    JwtAuthorizationFilter jwtAuthorizationFilter;
    
    • 1
    • 2

    并在configurer()方法中补充:

    // 将自定义的JWT过滤器添加在Spring Security框架内置的过滤器之前
    http.addFilterBefore(jwtAuthorizationFilter, UsernamePasswordAuthenticationFilter.class);
    
    • 1
    • 2

    52. 关于认证信息中的Principal

    关于SecurityContext中的认证信息,应该包含当事人(Principal)和权限(Authorities),其中,当事人(Principal)被声明为Object类型的,则可以使用任意数据类型作为当事人!

    在使用了Spring Security框架的项目中,当事人的数据是可以被注入到处理请求的方法中的!所以,使用哪种数据作为当事人,主要取决于“你在编写控制器中处理请求的方法时,需要通过哪些数据来区分当前登录的用户”。

    通常,使用自定义的数据类型作为当事人,并在此类型中封装关键数据,例如idusername等。

    则在security包下创建LoginPrincipal类:

    @Data
    public class LoginPrincipal implements Serializable {
        private Long id;
        private String username;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在JWT过滤器创建认证信息时,使用以上类型的对象作为认证信息中的当事人:

    LoginPrincipal loginPrincipal = new LoginPrincipal(); // 新增
    loginPrincipal.setId(id); // 新增
    loginPrincipal.setUsername(username); // 新增
    
    // 注意:以下调用构造方法时,第1个参数是以上创建的对象
    Authentication authentication = new UsernamePasswordAuthenticationToken(
            loginPrincipal, null, authorities);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    完成后,在当前项目任何控制器中任何处理请求的方法上,都可以添加@AuthenticationPrincipal LoginPrincipal loginPrincipal参数(与原有的其它参数不区分先后顺序),此参数的值就是以上过滤器中存入到认证信息中的当事人,所以,可以通过这种做法,在处理请求时识别当前登录的用户:

    @ApiOperation("删除管理员")
    @ApiOperationSupport(order = 200)
    @ApiImplicitParam(name = "id", value = "管理员id", required = true, dataType = "long")
    @PostMapping("/{id:[0-9]+}/delete")
    public JsonResult<Void> delete(@PathVariable Long id,
            // ===== 以下是新增的方法参数 =====
            @ApiIgnore @AuthenticationPrincipal LoginPrincipal loginPrincipal) {
        log.debug("开始处理【删除管理员】的请求,参数:{}", id);
        log.debug("当前登录的当事人:{}", loginPrincipal); // 新增,可以控制台观察数据
        adminService.delete(id);
        return JsonResult.ok();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    53. 关于CORS与PreFlight

    如果客户端向服务器端提交请求,在跨域的前提下,如果提交的请求配置了请求头中的非典型参数,例如配置了Authorization,此请求会被视为“复杂请求”,则会要求执行“预检”(PreFlight),如果预检不通过,则会导致跨域请求错误!

    关于预检,浏览器会自动向服务器端提交OPTIONS类型的请求执行预检,为了确保预检通过,不影响处理正常的请求,需要在SecurityConfigurationconfigurer()方法中对预检请求放行,可以采取的解决方案有:

    http.authorizeRequests()
                    .antMatchers(urls)
                    .permitAll()
    
                    // 以下2行代码是用于对预检的OPTIONS请求直接放行的
                    .antMatchers(HttpMethod.OPTIONS, "/**")
                    .permitAll()
    
                    .anyRequest()
                    .authenticated();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    或者,也可以:

    http.cors(); // 启用Spring Security框架的处理跨域的过滤器,此过滤器将放行跨域请求,包括预检的OPTIONS请求
    
    • 1

    则客户端可以携带复杂请求头进行访问:

    loadAdminList() {
      console.log('loadAdminList ...');
      let url = 'http://localhost:9081/admins';
      console.log('url = ' + url);
      this.axios
          .create({
            'headers': {
              'Authorization': localStorage.getItem('jwt')
            }
          })
          .get(url).then((response) => {
        let responseBody = response.data;
        console.log(responseBody);
        this.tableData = responseBody.data;
      });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
  • 相关阅读:
    高数_第3章重积分_三重积分的奇偶性
    ARouter - 组件化通信方案
    【内存管理】C与C++的内存管理异同点
    Worthington丨Worthington核糖核酸酶A和B介绍
    lua字符串表的高级用法
    第十章:枚举类与注解
    建站系列(八)--- 本地开发环境搭建(WNMP)
    Taro React组件开发(1) —— Overlay 遮罩层【渐入渐出动画遮罩层】
    栈(Stack) · 队列(Queue) · 循环队列 · 双端队列
    Rust学习入门
  • 原文地址:https://blog.csdn.net/weixin_43121885/article/details/127602664