文章目录
    
   本篇将介绍 SpringSecurity 结合 JWT (
   
    J
   
   son
   
    W
   
   eb
   
    T
   
   oken) 实现身份认证和权限验证.
   
   
    代码仓库
   
    
    
    概念
   
    
    
    什么是 JWT
   
    JWT 是一种 access-token. 内容是用英文点号连接起来的分成三个部分 (Header, Payload, Signature) 的字符串.
    
    相较于 Session 技术, 通过 JWT 我们可以提供无状态的 Web 服务. 服务器不用再保存客户端的信息, 扩展性大大增强.
    
    客户端把 JWT 保存到 localStorage 中, 每次对后端的请求都将 JWT 放到请求头中:
    
     Authorization: Bearer <token>
    
    , 服务端校验 JWT 中的信息完成身份验证.
    
    出于安全考虑, JWT 不应存在敏感信息, 应保持时效性 (过期机制) 并始终配合 Https 使用.
    
    
     参考链接
    
   
    
    
    HTTP Authentication Bearer
   
    
     参考链接
    
    
    认证 (Authentication) 是构建 Web 应用的不可或缺的一环, 简单介绍一下常见的 HTTP Authentication 方式 (
    
     Authentication Schemas
    
    ):
    
    ※ Basic (RFC 7617)
    
    ※ Bearer (
    
     RFC 6750
    
    )
    
    ※ Digest (
    
     RFC 7616
    
    )
    
    ※ HOBA (
    
     RFC 7486
    
    )
    
    ※
    
     Mutual
    
    
    ※
    
     AWS4-HMAC-SHA256
    
    
    JWT 是一种 token 格式, 而 Bearer 是一种认证方案. HTTP 的请求头 Authorization 的值的格式就是
    
     Authorization: <type> <credentials>
    
    的形式, 支持多种鉴权方案, Bearer 只是其中一种 (通常搭配 JWT, 在 JWT 的前面加上
    
     Bearer
    
    , 这个值就是
    
     Bearer Token
    
    ).
   
    
    
    思路
   
我们设计三个端点:
- 
     
 /auth/login
 
 : 登陆的端点. 前端携带有效用户信息换取 access-token (Json Web Token) 的端点;
- 
     
 /auth/register
 
 : 注册的端点. 该端点不会被身份认证和权限校验机制拦截, 是一个被放行的端点;
- 
     
 /task/1
 
 : 后续业务端点. 只有携带了有效 access-token 并有权限的请求才能访问此类端点;
    由于还是采用了 SpringBoot Web + SpringSecurity 的架构, 仍然是基于过滤器的, 我们把身份认证的过滤器命名为
    
     JWTAuthenticationFilter
    
    , 权限校验的过滤器命名为
    
     JWTAuthorizationFilter
    
    . 且前者要先于后者执行.
    
    来看一张描述这一过程的时序图:
    
    首先, 请求通过 /auth/login 端点进行身份认证, 成功后会在响应头中置入 access-token;
    
    随后, 获得 access-token 的请求同样在请求头中携带 access-token 访问后续服务. 这一过程会被权限校验过滤器拦截, 该过滤器会校验 access-token 是否有权限访问目标资源, 如果有就放行.
    
     
   
    
    
    实现
   
    为了实现这一思路, 我们首先需要有一张用户表, 表结构和数据如下
    
     (在本文中, 我们只需要简单定义一张 User 表, 后续文章会有比较完备的实现)
    
    :
    
     
   
    
    
    模块结构
   
cn
  └─caplike
      └─demo
          └─spring
              └─security
                  └─jwt
                      │  JWTApplication.java
                      │
                      ├─configuration
                      │      SecurityConfiguration.java
                      │      UsernamePasswordAuthenticationProvider.java
                      │
                      ├─controller
                      │      AuthController.java
                      │      TaskController.java
                      │
                      ├─domain
                      │  ├─dto
                      │  │      CustomUserDetails.java
                      │  │
                      │  └─entity
                      │          User.java
                      │
                      ├─filter
                      │      JWTAuthenticationFilter.java
                      │      JWTAuthorizationFilter.java
                      │
                      ├─mapper
                      │      UserMapper.java
                      │
                      ├─service
                      │      UserDetailsServiceImpl.java
                      │
                      └─util
                              JWTUtils.java
    和
    
     上一篇
    
    一致, 仍然从数据库中获取用户信息. 为此, 首先还是先来写配置
   
    
    
    SecurityConfiguration
   
    
     extends WebSecurityConfigurerAdapter
    
    
    在
    
     configure(AuthenticationManagerBuilder)
    
    中配置身份管理器, 告诉 Security 用什么途径, 从什么地方读取用户信息. 本文采用实现
    
     AuthenticationProvider
    
    接口的方式. 比起简单地指定
    
     UserDetailsService
    
    (
    
     auth.userDetailsService(T userDetailsService)
    
    ) 的方式, 自定义的
    
     AuthenticationProvider
    
    更加灵活.
   
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    private UsernamePasswordAuthenticationProvider usernamePasswordAuthenticationProvider;
    @Override
    protected void configure(AuthenticationManagerBuilder auth) {
        // 指定自定义的 AuthenticationProvider
        auth.authenticationProvider(usernamePasswordAuthenticationProvider);
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                // ref: demo-spring-security-csrf, 暂时禁用 csrf, 将在下一篇文章中细说
                .csrf().disable()
                .authorizeRequests()
                .antMatchers("/auth/**").permitAll()
                .anyRequest().hasAnyAuthority("ADMIN") // .authenticated()
                .and()
                .addFilter(new JWTAuthenticationFilter(authenticationManager()))
                // 让校验 Token 的过滤器在身份认证过滤器之后
                .addFilterAfter(new JWTAuthorizationFilter(), JWTAuthenticationFilter.class)
                // 既然启用 JWT, 那就彻底点, 不需要 Session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
    // ~ Autowired
    // -----------------------------------------------------------------------------------------------------------------
    @Autowired
    public void setUsernamePasswordAuthenticationProvider(UsernamePasswordAuthenticationProvider usernamePasswordAuthenticationProvider) {
        this.usernamePasswordAuthenticationProvider = usernamePasswordAuthenticationProvider;
    }
}
    
    
    AuthenticationProvider
   
    SpringSecurity 提供了一系列选项完成认证, 归根结底, 这一过程的核心可以被描述为: 认证请求会被
    
     org.springframework.security.authentication.AuthenticationProvider
    
    处理, 最终会返回一个完整的, 认证过的认证对象:
    
     org.springframework.security.core.Authentication
    
    的实现 (这里我们将会采用
    
     UsernamePasswordAuthenticationToken
    
    ):
    
    
    
    我们继承
    
     AuthenticationProvider
    
    接口, 实现自己的处理
    
     UsernamePasswordAuthenticationToken
    
    的
    
     UsernamePasswordAuthenticationProvider
    
    类:
   
@Component
public class UsernamePasswordAuthenticationProvider implements AuthenticationProvider {
    /**
     * @see cn.caplike.demo.spring.security.jwt.service.UserDetailsServiceImpl
     */
    private UserDetailsService userDetailsService;
    /**
     * 加密器
     */
    private BCryptPasswordEncoder bCryptPasswordEncoder;
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // 获取用户输入的用户名和密码
        String username = authentication.getName();
        String password = authentication.getCredentials().toString();
        // 获取封装用户信息的对象
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        // 进行密码的比对
        boolean flag = bCryptPasswordEncoder.matches(password, userDetails.getPassword());
        // 校验通过
        if (flag) {
            // 将权限信息也封装进去
            return new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities());
        }
        throw new AuthenticationException("用户密码错误") {
        };
    }
    @Override
    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }
    // ~ Autowired
    // -----------------------------------------------------------------------------------------------------------------
    @Autowired
    public void setUserDetailsService(@Qualifier("userDetailsServiceImpl") UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }
    @Autowired
    public void setBCryptPasswordEncoder(BCryptPasswordEncoder bCryptPasswordEncoder) {
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
    }
    // ~ Bean
    // -----------------------------------------------------------------------------------------------------------------
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
    
     AuthenticationProvider
    
    接口定义了两个方法:
    
    
     Authentication authenticate(Authentication authentication)
    
    用于执行认证. 接收一个认证请求对象, 返回一个完整的认证过的对象.
    
    
     boolean suports(Class<?> authentication)
    
    控制对于何种
    
     Authentication
    
    的实现, 启用当前
    
     AuthenticationProvider
    
    , 由于我们采用
    
     UsernamePasswordAuthenticationToken
    
    , 所以在这里也作出限制.
    
    
     BCryptPasswordEncoder
    
    是官方提供的其中一种密钥加密器, 用户的密码当然是加密存放了. (当然也可以实现其接口
    
     PasswordEncoder
    
    自行实现加密规则)
   
    
    
    UserDetailsService
   
    
     UserDetailsService
    
    提供了获取用户信息的方法 (
    
     UserDetails loadUserByUsername(String)
    
    ) 定义.
    
    
     UserDetails
    
    接口定义了用户核心信息, 它的实现类不会被 SpringSecurity 直接处理, 而会被封装到
    
     Authentication
    
    的实现类中, 后者的
    
     Object getPrincipal();
    
    返回值的真实类型就是
    
     UserDetails
    
    的实现. 在本文中就是
    
     CustomUserDetails
    
    .
   
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    private UserMapper userMapper;
    /**
     * Description: 从数据库获取 UserDetails
     *
     * @param username 用户名
     * @return org.springframework.security.core.userdetails.UserDetails
     * @throws UsernameNotFoundException 当用户不存在
     * @author LiKe
     * @date 2020-04-26 08:59:01
     * @see UserDetailsService#loadUserByUsername(String)
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return new CustomUserDetails(Optional.ofNullable(userMapper.findByUsername(username)).orElseThrow(() -> new UsernameNotFoundException("用户不存在!")));
    }
    @Autowired
    public void setUserMapper(UserMapper userMapper) {
        this.userMapper = userMapper;
    }
}
    
    
    JWTAuthenticationFilter & JWTAuthorizationFilter
   
    从
    
     SecurityConfiguration
    
    的代码中可以看到, 我们配置了对
    
     /auth/**
    
    放行, 其中包含两个端点:
    
     /auth/login
    
    和
    
     /auth/register
    
    .
   
    
    
    JWTAuthenticationFilter
   
    
     extends UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter
    
    . 是身份验证的过滤器. 主要职责是完成对请求中携带的用户信息的校验并颁发 access-token.
    
    
     UsernamePasswordAuthenticationFilter
    
    定义了过滤器的基本逻辑, 在构造方法中可以看到该过滤器默认对 POST 方式, 且 URI 是
    
     /login
    
    的请求才会拦截:
   
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
public UsernamePasswordAuthenticationFilter() {
	super(new AntPathRequestMatcher("/login", "POST"));
}
    我们期望的请求是携带形如
    
     {"name": "", "password": ""}
    
    的用户身份数据到后端的, 所以需要把默认的 usernameParameter 从 username 改成 name, 并把这个 URI 改成我们期望的
    
     /auth/login
    
    , 认证成功后会将 access-token 放在响应头中回执给前端. 完整的
    
     JWTAuthenticationFilter
    
    如下:
   
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    private final AuthenticationManager authenticationManager;
    public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
        // 浏览器访问 http://localhost:18902/auth/login 会通过 JWTAuthenticationFilter
        super.setFilterProcessesUrl("/auth/login");
        super.setUsernameParameter("name");
    }
    @SneakyThrows
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        // 数据是通过 requestBody 传输
        User user = JSON.parseObject(request.getInputStream(), StandardCharsets.UTF_8, User.class);
        return authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(user.getName(), user.getPassword())
        );
    }
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
                                            FilterChain chain,
                                            Authentication authResult) {
        log.debug("authentication filter successful authentication: {}", authResult);
        // 如果验证成功, 就生成 Token 并返回
        CustomUserDetails customUserDetails = (CustomUserDetails) authResult.getPrincipal();
        response.setHeader("access-token",
                JWTUtils.TOKEN_PREFIX + JWTUtils.create(customUserDetails.getName(), false, customUserDetails));
    }
    /**
     * 如果 attemptAuthentication 抛出 AuthenticationException 则会调用这个方法
     *
     * @see UsernamePasswordAuthenticationFilter#unsuccessfulAuthentication(HttpServletRequest, HttpServletResponse, AuthenticationException)
     */
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
                                              AuthenticationException failed) throws IOException {
        log.debug("authentication filter unsuccessful authentication: {}", failed.getMessage());
        response.getWriter().write("authentication failed, reason: " + failed.getMessage());
    }
}
    
     authenticationManager.authenticate
    
    方法最终会调用在
    
     SecurityConfiguration#configure(AuthenticationManagerBuilder)
    
    中指定的
    
     AuthenticationProvider
    
    的
    
     authenticate
    
    从而执行
    
     UserDetailsService
    
    的
    
     loadUserByUsername
    
    获取用户信息.
    
    
     super.setFilterProcessesUrl("/auth/login");
    
    表明这个过滤器会且仅会监听
    
     /auth/login
    
    端点.
    
    
     JWTAuthenticationFilter
    
    覆盖了
    
     UsernamePasswordAuthenticationFilter
    
    的
   
- 
     
 Authentication attemptAuthentication
 
- 
     
 void successfulAuthentication
 
- 
     
 void unsuccessfulAuthentication
 
 .
介绍一下三个方法的调用逻辑:
- 
     一个请求到来, 首先会验证这个请求是否应当被这个过滤器拦截. 不满足条件则会直接被放行. 依据就是
 
 UsernamePasswordAuthenticationFilter
 
 的构造方法中传入的
 
 AntPathRequestMatcher
 
 , 在我们自己的
 
 JWTAuthenticationFilter
 
 中通过
 
 setFilterProcessesUrl
 
 指定了
 
 /auth/login
 
 , 这个方法会通过超类的
 
 setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher(filterProcessesUrl));
 
 设置
 
 AntPathRequestMatcher
 
 ;
- 
     紧接着会调用
 
 attemptAuthentication
 
 执行验证, 这个方法期望返回一个
 
 Authentication
 
 的实现类, 返回 null 表示认证没有完成.
- 
     如果
 
 attemptAuthentication
 
 抛出
 
 AuthenticationException
 
 异常则会触发
 
 unsuccessfulAuthentication
 
 方法, 并且异常本身会作为目标方法的第三个参数传入;
- 
     最后, 在以上几步都通过的情况下会调用
 
 successfulAuthentication
 
 方法, 并将
 
 attemptAuthentication
 
 认证结果对象作为参数传入.
 
 剩下的部分, 有前面的说明, 相信
 
 JWTAuthenticationFilter
 
 看起来就很简单了.
    
    
    JWTAuthorizationFilter
   
    这个过滤器用于对携带 access-token 的请求执行权限检查. 所以, 除了注册端点之外的所有请求都应该被它过滤.
    
    理论上在认证成功后我们应该把 access-token 缓存起来, 并设置合适的过期时间. 当携带有效 access-token 的请求到来的时候, 也应该适时地延长该令牌的过期时间. 关于这部分逻辑, 后续文章会有更为详细的实现.
    
    完整代码如下:
   
public class JWTAuthorizationFilter extends OncePerRequestFilter {
    private static final Set<String> WHITE_LIST = Stream.of("/auth/register").collect(Collectors.toSet());
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        log.debug("authorization filter doFilterInternal");
        final String authorization = request.getHeader(JWTUtils.TOKEN_HEADER);
        log.debug("raw-access-token: {}", authorization);
        // Branch A: 如果请求头中没有 Authorization
        if (StringUtils.isBlank(authorization)) {
            // 白名单放行
            if (WHITE_LIST.contains(request.getRequestURI())) {
                chain.doFilter(request, response);
            } else {
                response.getWriter().write("未经授权的访问!");
            }
            return;
        }
        // Branch B: 如果请求头中有 Bear xxx, 设置认证信息
        final String jsonWebToken = authorization.replace(JWTUtils.TOKEN_PREFIX, StringUtils.EMPTY);
        // TODO 用 Redis 的过期控制 token, 而不用 jwt 的 Expiration
        // if (JWTUtils.hasExpired(jsonWebToken)) {
        //     response.getWriter().write("access-token 已过期, 请重新登陆!");
        // }
        // TODO 每一次携带正确 token 的访问, 都刷新 Redis 的过期时间
        CustomUserDetails customUserDetails = JWTUtils.userDetails(jsonWebToken);
        SecurityContextHolder.getContext().setAuthentication(
                new UsernamePasswordAuthenticationToken(
                        customUserDetails.getName(),
                        // TODO Json Web Token 中不能携带用户密码
                        customUserDetails.getPassword(),
                        customUserDetails.getAuthorities()
                )
        );
        chain.doFilter(request, response);
    }
}
    在
    
     doFilterInterneal
    
    方法最后, 我们把存在 JWT 中的用户信息取出, 构造了一个完整的
    
     Authentication
    
    对象. 其中,
    
     getAuthorities
    
    方法体现用户权限信息. Security 会根据
    
     SecurityContext
    
    中的
    
     Authentication
    
    的
    
     authorities
    
    判断当前用户是否有权限访问目标资源.
   
    
    
    其他说明
   
    
    
    UserMapper
   
ORM 框架本例采用了 MyBatis. 主要是用于获取用户相关信息.
@Repository
@Mapper
public interface UserMapper {
    /**
     * Description: 查找 User
     *
     * @param username 用户名
     * @return cn.caplike.demo.spring.security.jwt.domain.entity.User
     * @author LiKe
     * @date 2020-04-22 09:04:06
     */
    @Select("SELECT * FROM USER WHERE name = #{username}")
    User findByUsername(String username);
    /**
     * Description: 新建 User
     *
     * @param user {@link User}
     * @return cn.caplike.demo.spring.security.jwt.domain.entity.User
     * @author LiKe
     * @date 2020-04-22 11:18:21
     */
    @Insert("INSERT INTO USER(name, password, role) VALUES (#{name}, #{password}, #{role})")
    int save(User user);
}
    
    
    JWTUtils
   
是实现生成和解析 Json Web Token 的工具类. 需要引入相关依赖:
<!-- JWT -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.1</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.1</version>
    <scope>runtime</scope>
</dependency>
代码如下:
public final class JWTUtils {
    public static final String TOKEN_HEADER = "Authorization";
    public static final String TOKEN_PREFIX = "Bearer ";
    private static final String SECRET = "5ae95dd3c5f811b9b819434910c52820ae7cfb3d9f7961e7117b24a7012873767d79f61f81fc2e06ebb6fd4f09ab47764d6e20607f843c22a0a2a6a6ed829680";
    /**
     * 签发人
     */
    private static final String ISSUER = "caplike";
    private JWTUtils() {
    }
    /**
     * Description: 创建 Json Web Token
     *
     * @param username    {String} 用户名
     * @param rememberMe  {boolean} 是否记住我
     * @param userDetails {@link CustomUserDetails} 的实现类
     * @return java.lang.String Json Web Token
     * @author LiKe
     * @date 2020-04-21 16:18:10
     */
    public static String create(String username, boolean rememberMe, CustomUserDetails userDetails) {
        return Jwts.builder()
                // [Attention] 要先 setClaims(初始化底层 map) 再设置 subject, 如果 subject 先设置, 会被覆盖.
                .setClaims(JSON.parseObject(JSON.toJSONString(userDetails)))
                // 主题
                .setSubject(username)
                // 颁发时间
                .setIssuedAt(Date.from(LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant()))
                // 颁发人
                .setIssuer(ISSUER)
                .signWith(Keys.hmacShaKeyFor(SECRET.getBytes()), SignatureAlgorithm.HS512)
                .serializeToJsonWith(map -> JSON.toJSONBytes(map))
                .compact();
    }
    /**
     * Description: 获得 subject
     *
     * @param jwt {String} Json Web Token
     * @return java.lang.String subject
     * @author LiKe
     * @date 2020-04-21 18:09:26
     */
    public static String subject(String jwt) {
        return claims(jwt).getSubject();
    }
    public static CustomUserDetails userDetails(String jwt) {
        return JSON.parseObject(JSON.toJSONString(claims(jwt)), CustomUserDetails.class);
    }
    private static Claims claims(String jwt) {
        return Jwts.parserBuilder()
                .setSigningKey(Keys.hmacShaKeyFor(SECRET.getBytes()))
                .deserializeJsonWith(bytes -> JSONObject.parseObject(new String(bytes), new TypeReference<Map<String, Object>>() {
                }))
                .build()
                .parseClaimsJws(jwt)
                .getBody();
    }
}
    
    
    测试
   
    最后, 来测试一波. 如之前所述, 本例提供两个
    
     Controller
    
    :
    
    
     AuthController
    
@RestController
@RequestMapping("/auth")
public class AuthController {
    private UserMapper userMapper;
    private BCryptPasswordEncoder bCryptPasswordEncoder;
    /**
     * Description: 注册
     *
     * @param registerUser 用户信息
     * @return java.lang.String
     * @author LiKe
     * @date 2020-04-24 09:22:54
     * @see cn.caplike.demo.spring.security.jwt.filter.JWTAuthenticationFilter
     * @see cn.caplike.demo.spring.security.jwt.filter.JWTAuthorizationFilter
     */
    @PostMapping("/register")
    public User registerUser(@RequestBody Map<String, String> registerUser) {
        User user = new User();
        user.setName(registerUser.get("name"));
        // 记得注册的时候把密码加密一下
        user.setPassword(bCryptPasswordEncoder.encode(registerUser.get("password")));
        user.setRole("USER");
        log.debug("AuthController: {}", userMapper.save(user));
        return user;
    }
    @Autowired
    public void setBCryptPasswordEncoder(BCryptPasswordEncoder bCryptPasswordEncoder) {
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
    }
    @Autowired
    public void setUserMapper(UserMapper userMapper) {
        this.userMapper = userMapper;
    }
}
    
     TaskController
    
@RestController
@RequestMapping("/task")
public class TaskController {
    @GetMapping
    public String listTasks() {
        return "任务列表";
    }
    @PostMapping
    public String newTasks() {
        return "创建了一个新的任务";
    }
    @PutMapping("/{taskId}")
    public String updateTasks(@PathVariable("taskId") Integer id) {
        return "更新了一下 id: " + id + " 的任务";
    }
    @DeleteMapping("/{taskId}")
    public String deleteTasks(@PathVariable("taskId") Integer id) {
        return "删除了 id: " + id + " 的任务";
    }
}
启动项目用 Postman 测试:
    
    
    登陆请求
   
    
    
    控制台输出:
   
... .f.JWTAuthenticationFilter    : Request is to process authentication
... .CustomAuthenticationProvider : CustomAuthenticationProvider: supports: class org.springframework.security.authentication.UsernamePasswordAuthenticationToken
... .m.UserMapper.findByUsername  : ==>  Preparing: SELECT * FROM USER WHERE name = ? 
... .m.UserMapper.findByUsername  : ==> Parameters: root(String)
... .m.UserMapper.findByUsername  : <==      Total: 1
... .f.JWTAuthenticationFilter    : authentication filter successful authentication: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@d0448fa4: Principal: CustomUserDetails(name=root, password=$2a$10$yjx08FV8gARdM7YinjFp7u.aD3dyDYgBwzWl84qYFLFLNhn3R1Vs2, authorities=[ADMIN]); Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ADMIN
    
    
    访问资源
   
    用合法的 access-token 访问后端资源
    
    
    
    控制台输出:
   
... .JWTAuthorizationFilter     : authorization filter doFilterInternal
... .JWTAuthorizationFilter     : raw-access-token: Bearer eyJhbGciOiJIUzUxMiJ9.eyJwYXNzd29yZCI6IiQyYSQxMCR5angwOEZWOGdBUmRNN1lpbmpGcDd1LmFEM2R5RFlnQnd6V2w4NHFZRkxGTE5objNSMVZzMiIsImNyZWRlbnRpYWxzTm9uRXhwaXJlZCI6dHJ1ZSwibmFtZSI6InJvb3QiLCJhY2NvdW50Tm9uRXhwaXJlZCI6dHJ1ZSwiYXV0aG9yaXRpZXMiOlt7ImF1dGhvcml0eSI6IkFETUlOIn1dLCJlbmFibGVkIjp0cnVlLCJhY2NvdW50Tm9uTG9ja2VkIjp0cnVlLCJ1c2VybmFtZSI6InJvb3QiLCJzdWIiOiJyb290IiwiaWF0IjoxNTg5NTI2MDI1LCJpc3MiOiJjYXBsaWtlIn0.bui-_EX5S_tkoT94dMQoavkRVJZV0Yq9_-JMlS30sRRP4-F0DLB3TLxU4w2MU2pVm4vrFfk8JCyrtCJRYE9B0Q
    把数据库中 root 用户的权限改成 USER, 重启服务, 重新登陆 (获取新权限下的 access-token) 再次尝试访问
    
     /task/1
    
    端点:
    
    
    
    返回 403:
   
{
    "timestamp": "2020-05-15T08:10:17.767+0000",
    "status": 403,
    "error": "Forbidden",
    "message": "Forbidden",
    "path": "/jwt/task/1"
}
    
    
    总结
   
    本文详细介绍了如何用 JWT 做 access-token, 依托 SpringSecurity 完成用户身份认证和权限校验.
    
    但是仍有几处不足:
   
- csrf 在本例中未作处理, 而是直接禁用的, 显然不够安全;
- access-token 没有缓存未作过期;
- 权限是写死的, 每次更改权限需要重启服务;
- 整个模块返回信息结构仍然是框架提供的默认结构, 很多场合下我们更希望用自己统一的结构返回给前端, 无论是异常还是正常;
这些问题都会在接下来的文章中探讨.
– END –
    
    
    Reference
   
 
