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


本篇将介绍 SpringSecurity 结合 JWT ( Json Web Token) 实现身份认证和权限验证.
代码仓库

概念

什么是 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 接口, 实现自己的处理 UsernamePasswordAuthenticationTokenUsernamePasswordAuthenticationProvider 类:

@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) 中指定的 AuthenticationProviderauthenticate 从而执行 UserDetailsServiceloadUserByUsername 获取用户信息.
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 中的 Authenticationauthorities 判断当前用户是否有权限访问目标资源.

其他说明

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

  • 4
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
Spring Boot是一个基于Spring框架的快速开发Web应用程序的框架,Spring SecuritySpring框架的安全模块,JWT是一种用于身份验证的开放标准。Vue是一种流行的JavaScript框架,用于构建用户界面。 结合这些技术,可以实现前后端分离的登录、权限管理和Token管理。具体步骤如下: 1. 在Spring Boot项目中导入Spring SecurityJWT的Maven依赖。 2. 配置Spring Security,包括创建用户、角色和权限等。 3. 创建一个JWT工具类,用于生成和解析Token。 4. 创建一个登录接口,接收用户名和密码,验证用户信息,生成Token并返回给前端。 5. 创建一个Token验证过滤器,用于验证请求中的Token是否有效。 6. 在Vue项目中使用Axios发送登录请求,获取Token并保存到本地存储中。 7. 在Vue项目中使用Vue Router和VueX进行路由和状态管理。 8. 创建一个路由守卫,用于验证用户是否登录和是否有权限访问某些页面。 9. 在需要进行身份验证的请求中添加Token。 下面是一个简单的示例代码,仅供参考: 后端代码: ```java // 配置Spring Security @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Autowired private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public JwtAuthenticationFilter jwtAuthenticationFilter() { return new JwtAuthenticationFilter(); } @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers("/api/auth/**").permitAll() .anyRequest().authenticated() .and() .exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint) .and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); } } // 创建一个JWT工具类 public class JwtUtils { private static final String SECRET_KEY = "mySecretKey"; private static final long EXPIRATION_TIME = 86400000; // 24 hours public static String generateToken(UserDetails userDetails) { Map<String, Object> claims = new HashMap<>(); return Jwts.builder() .setClaims(claims) .setSubject(userDetails.getUsername()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) .signWith(SignatureAlgorithm.HS512, SECRET_KEY) .compact(); } public static String getUsernameFromToken(String token) { return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody().getSubject(); } public static boolean validateToken(String token, UserDetails userDetails) { String username = getUsernameFromToken(token); return username.equals(userDetails.getUsername()) && !isTokenExpired(token); } private static boolean isTokenExpired(String token) { Date expiration = Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody().getExpiration(); return expiration.before(new Date()); } } // 创建一个登录接口 @RestController @RequestMapping("/api/auth") public class AuthController { @Autowired private AuthenticationManager authenticationManager; @Autowired private UserDetailsService userDetailsService; @PostMapping("/login") public ResponseEntity<?> authenticateUser(@RequestBody LoginRequest loginRequest) { Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword())); SecurityContextHolder.getContext().setAuthentication(authentication); UserDetails userDetails = userDetailsService.loadUserByUsername(loginRequest.getUsername()); String token = JwtUtils.generateToken(userDetails); return ResponseEntity.ok(new JwtAuthenticationResponse(token)); } } // 创建一个Token验证过滤器 public class JwtAuthenticationFilter extends OncePerRequestFilter { @Autowired private UserDetailsService userDetailsService; @Autowired private JwtUtils jwtUtils; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String header = request.getHeader("Authorization"); if (header != null && header.startsWith("Bearer ")) { String token = header.substring(7); String username = jwtUtils.getUsernameFromToken(token); UserDetails userDetails = userDetailsService.loadUserByUsername(username); if (jwtUtils.validateToken(token, userDetails)) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } filterChain.doFilter(request, response); } } // 创建一个自定义的AuthenticationEntryPoint @Component public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"); } } // 创建一个自定义的UserDetailsService @Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserRepository userRepository; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username); if (user == null) { throw new UsernameNotFoundException("User not found with username: " + username); } return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), new ArrayList<>()); } } // 创建一个实体类User和一个接口UserRepository @Entity @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "username") private String username; @Column(name = "password") private String password; // getters and setters } @Repository public interface UserRepository extends JpaRepository<User, Long> { User findByUsername(String username); } ``` 前端代码: ```javascript // 在Vue项目中使用Axios发送登录请求 axios.post('/api/auth/login', { username: 'admin', password: 'password' }).then(response => { localStorage.setItem('token', response.data.token); }); // 在需要进行身份验证的请求中添加Token axios.get('/api/users', { headers: { Authorization: 'Bearer ' + localStorage.getItem('token') } }); // 创建一个路由守卫 router.beforeEach((to, from, next) => { const publicPages = ['/login', '/register']; const authRequired = !publicPages.includes(to.path); const loggedIn = localStorage.getItem('token'); if (authRequired && !loggedIn) { return next('/login'); } next(); }); // 使用VueX进行状态管理 const store = new Vuex.Store({ state: { isLoggedIn: !!localStorage.getItem('token') }, mutations: { login(state) { state.isLoggedIn = true; }, logout(state) { state.isLoggedIn = false; } }, actions: { login({ commit }) { return new Promise(resolve => { axios.post('/api/auth/login', { username: 'admin', password: 'password' }).then(response => { localStorage.setItem('token', response.data.token); commit('login'); resolve(); }); }); }, logout({ commit }) { return new Promise(resolve => { localStorage.removeItem('token'); commit('logout'); resolve(); }); } } }); ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值