实现前后端分离模式的登录接口

2022-09-05 SpringSecurity

现在都是前后端分离的开发方式,我们需要自定义登陆接口,然后让SpringSecurity对这个接口放行,让用户访问这个接口的时候不用登录也能访问。

# 引入jwt工具生成Token

前后端分离的开发模式下,后端使用jwt生成Token,前端登录获取Token,我们使用Hutool工具包的JWT工具 (opens new window)生成Token

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.5</version>
</dependency>

生成Token

// 密钥
byte[] key = "1234567890".getBytes();

String token = JWT.create()
    .setPayload("id", "1") 
    .setPayload("username", "user1")
    .setPayload("admin", true)
    .setKey(key)
    .sign();

解析Token

String rightToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9……………………";
JWT jwt = JWT.of(rightToken);

jwt.getPayload("id");// 1
jwt.getPayload("username");// user1

# 开发JWT工具类

package com.xk857.jenkinsdemo.utils;

import cn.hutool.jwt.JWT;
import com.xk857.jenkinsdemo.domain.LoginUser;
import com.xk857.jenkinsdemo.domain.User;
import java.util.Date;

public class JwtUtils {
    // 密钥
    private static final byte[] TOKEN_KEY = "xk857.com".getBytes();
    
    // token过期时间一周
    private static final long EXPIRE = 60000 * 60 * 24 * 7;

    /**
     * 根据用户信息,生成令牌
     * @param loginUser 用户对象
     * @return token
     */
    public static String geneJsonWebToken(LoginUser loginUser) {
        // 默认使用HS256加密算法
        return JWT.create()
                .setPayload("id", loginUser.getUser().getId())
                .setPayload("username", loginUser.getUser().getUsername())
                .setPayload("nickName", loginUser.getUser().getNickName())
                .setIssuedAt(new Date())
                .setExpiresAt(new Date(System.currentTimeMillis() + EXPIRE))
                .setKey(TOKEN_KEY).sign();
    }

    /**
     * 解析Token
     */
    public static User checkJWT(String token) {
        JWT jwt = JWT.of(token);
        User user = new User();
        user.setId((String) jwt.getPayload("id"));
        user.setUsername((String) jwt.getPayload("username"));
        user.setNickName((String) jwt.getPayload("nickName"));
        return user;
    }
}

# 更改SpringSecurity默认配置

SpringSecurity默认开启csrf,我们因为是前后端分离开发,所以要把这个关闭,并允许登录接口匿名访问,另外还需要注入AuthenticationManager对象到Bean中,开发登录接口需要调用其方法。SpringBoot2.7.X写法如下:

@Configuration
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/user/login").anonymous()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();
        return http.build();
    }
    
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }
}

SpringBoot2.7.X之前的写法如下:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

   @Bean
   public PasswordEncoder passwordEncoder(){
       return new BCryptPasswordEncoder();
   }

   @Override
   protected void configure(HttpSecurity http) throws Exception {
       http
               //关闭csrf
               .csrf().disable()
               //不通过Session获取SecurityContext
               .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
               .and()
               .authorizeRequests()
               // 对于登录接口 允许匿名访问
               .antMatchers("/user/login").anonymous()
               // 除上面外的所有请求全部需要鉴权认证
               .anyRequest().authenticated();
   }

   @Bean
   @Override
   public AuthenticationManager authenticationManagerBean() throws Exception {
       return super.authenticationManagerBean();
   }
}

# 开发登录接口生成Token

@RestController
public class AuthController {
    
    @Autowired
    private AuthenticationManager authenticationManager;

    @PostMapping("/user/login")
    public Map<String,String> login(@RequestBody LoginParam param) {
        // 使用SpringSecurity登录认证,通用写法
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(param.getUsername(),param.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        if(Objects.isNull(authenticate)){
            throw new RuntimeException("用户名或密码错误");
        }

        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        // 生成token
        String token = JwtUtils.geneJsonWebToken(loginUser);
        Map<String,String> map = new HashMap<>();
        map.put("token",token);
        return map;
    }
}

# 撰写认证过滤器

我们需要自定义一个过滤器,这个过滤器会去获取请求头中的token,对token进行解析,然后封装Authentication对象存入SecurityContextHolder

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //获取token
        String token = request.getHeader("token");
        if (!StringUtils.hasText(token)) {
            //放行
            filterChain.doFilter(request, response);
            return;
        }
        //解析token,获取User对象封装成LoginUser对象
        User user = JwtUtils.checkJWT(token);
        LoginUser loginUser = new LoginUser(user);
        //存入SecurityContextHolder
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, null);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //放行
        filterChain.doFilter(request, response);
    }
}

配置SpringSecurity,把token校验过滤器添加到过滤器链中

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/user/login").anonymous()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();
        //把token校验过滤器添加到过滤器链中
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }
}

# 登录接口测试

访问接口发现已经能正常访问了

image-20220905103659268

# 携带Token访问测试接口

image-20220905104417977

但是如果我们账号或密码故意输入错误,发现页面无反应。浏览器中访问测试接口提示403,403代表权限不足,我们想让它返回自定义的JSON数据该如何做呢?

上次更新: 5 个月前