SpringSecurity (3) SpringBoot + JWT 实现身份认证和权限验证

  • Post author:
  • Post category:其他


本篇将介绍 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

).



思路

我们设计三个端点:


  1. /auth/login

    : 登陆的端点. 前端携带有效用户信息换取 access-token (Json Web Token) 的端点;

  2. /auth/register

    : 注册的端点. 该端点不会被身份认证和权限校验机制拦截, 是一个被放行的端点;

  3. /task/1

    : 后续业务端点. 只有携带了有效 access-token 并有权限的请求才能访问此类端点;

由于还是采用了 SpringBoot Web + SpringSecurity 的架构, 仍然是基于过滤器的, 我们把身份认证的过滤器命名为

JWTAuthenticationFilter

, 权限校验的过滤器命名为

JWTAuthorizationFilter

. 且前者要先于后者执行.

来看一张描述这一过程的时序图:

首先, 请求通过 /auth/login 端点进行身份认证, 成功后会在响应头中置入 access-token;

随后, 获得 access-token 的请求同样在请求头中携带 access-token 访问后续服务. 这一过程会被权限校验过滤器拦截, 该过滤器会校验 access-token 是否有权限访问目标资源, 如果有就放行.

时序图 - 基于 JWT 的身份认证和权限校验



实现

为了实现这一思路, 我们首先需要有一张用户表, 表结构和数据如下

(在本文中, 我们只需要简单定义一张 User 表, 后续文章会有比较完备的实现)

:

表-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

):

Authentication 的实现

我们继承

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

    .

介绍一下三个方法的调用逻辑:

  1. 一个请求到来, 首先会验证这个请求是否应当被这个过滤器拦截. 不满足条件则会直接被放行. 依据就是

    UsernamePasswordAuthenticationFilter

    的构造方法中传入的

    AntPathRequestMatcher

    , 在我们自己的

    JWTAuthenticationFilter

    中通过

    setFilterProcessesUrl

    指定了

    /auth/login

    , 这个方法会通过超类的

    setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher(filterProcessesUrl));

    设置

    AntPathRequestMatcher

    ;
  2. 紧接着会调用

    attemptAuthentication

    执行验证, 这个方法期望返回一个

    Authentication

    的实现类, 返回 null 表示认证没有完成.
  3. 如果

    attemptAuthentication

    抛出

    AuthenticationException

    异常则会触发

    unsuccessfulAuthentication

    方法, 并且异常本身会作为目标方法的第三个参数传入;
  4. 最后, 在以上几步都通过的情况下会调用

    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 完成用户身份认证和权限校验.

但是仍有几处不足:

  1. csrf 在本例中未作处理, 而是直接禁用的, 显然不够安全;
  2. access-token 没有缓存未作过期;
  3. 权限是写死的, 每次更改权限需要重启服务;
  4. 整个模块返回信息结构仍然是框架提供的默认结构, 很多场合下我们更希望用自己统一的结构返回给前端, 无论是异常还是正常;

这些问题都会在接下来的文章中探讨.

– END –



Reference


Spring Security Authentication Provider



版权声明:本文为caplike原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。