【Spring Security + JWT实现前后端分离登录处理】

发布于:2023-01-22 ⋅ 阅读:(377) ⋅ 点赞:(0)

登录总体流程

前后端分离登录和常规后台登录的区别

1)前后端交互方式不同

​ 常规登录,通过页面的跳转和模型数据绑定

​ 前后端分离,通过Ajax和JSON进行通信

2)登录状态跟踪方式不同

​ 常规登录,通过Session和Cookie保存用户状态

​ 前后端分离,通过Token保持用户状态

总体流程

在这里插入图片描述

基于JWT和RSA的Token机制

JWT简介

JSON Web Tokens 是JSON格式的加密字符串,用于加密验证信息,在前后端进行通信

分为三个部分

1) 头部

2) 负载

3) 指纹

在这里插入图片描述

Java后台使用jjwt包来实现JWT的操作

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

<dependency>
    <groupId>joda-time</groupId>
    <artifactId>joda-time</artifactId>
    <version>2.9.9</version>
</dependency>

JWT工具类

/**
 * JWT工具类
 */
public class JwtUtil {

    public static final String JWT_KEY_USERNAME = "username";
    public static final int EXPIRE_MINUTES = 120;

    /**
     * 私钥加密token
     */
    public static String generateToken(String username,  PrivateKey privateKey, int expireMinutes) {

        return Jwts.builder()
                .claim(JWT_KEY_USERNAME, username)
                .setExpiration(DateTime.now().plusMinutes(expireMinutes).toDate())
                .signWith(SignatureAlgorithm.RS256, privateKey)
                .compact();
    }

    /**
     * 从token解析用户
     *
     * @param token
     * @param publicKey
     * @return
     * @throws Exception
     */
    public static String getUsernameFromToken(String token, PublicKey publicKey){
        Jws<Claims> claimsJws = Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
        Claims body = claimsJws.getBody();
        String username = (String) body.get(JWT_KEY_USERNAME);
        return username;
    }
}

RSA简介

RSA是一种非对称式的加密算法

对称式加密只有一个秘钥,加密和解密都通过该秘钥完成

非对称式加密有两个秘钥,公钥和私钥,加密和解密由公钥和私钥分开完成

/**
 * RSA工具类
 */
public class RsaUtil {

    public static final String RSA_SECRET = "blbweb@#$%"; //秘钥
    public static final String RSA_PATH = System.getProperty("user.dir")+"/rsa/";//秘钥保存位置
    public static final String RSA_PUB_KEY_PATH = RSA_PATH + "pubKey.rsa";//公钥路径
    public static final String RSA_PRI_KEY_PATH = RSA_PATH + "priKey.rsa";//私钥路径

    public static PublicKey publicKey; //公钥
    public static PrivateKey privateKey; //私钥

    /**
     * 类加载后,生成公钥和私钥文件
     */
    static {
        try {
            File rsa = new File(RSA_PATH);
            if (!rsa.exists()) {
                rsa.mkdirs();
            }
            File pubKey = new File(RSA_PUB_KEY_PATH);
            File priKey = new File(RSA_PRI_KEY_PATH);
            //判断公钥和私钥如果不存在就创建
            if (!priKey.exists() || !pubKey.exists()) {
                //创建公钥和私钥文件
                RsaUtil.generateKey(RSA_PUB_KEY_PATH, RSA_PRI_KEY_PATH, RSA_SECRET);
            }
            //读取公钥和私钥内容
            publicKey = RsaUtil.getPublicKey(RSA_PUB_KEY_PATH);
            privateKey = RsaUtil.getPrivateKey(RSA_PRI_KEY_PATH);
        } catch (Exception ex) {
            ex.printStackTrace();
            throw new RuntimeException(ex);
        }
    }

    /**
     * 从文件中读取公钥
     *
     * @param filename 公钥保存路径,相对于classpath
     * @return 公钥对象
     * @throws Exception
     */
    public static PublicKey getPublicKey(String filename) throws Exception {
        byte[] bytes = readFile(filename);
        return getPublicKey(bytes);
    }

    /**
     * 从文件中读取密钥
     *
     * @param filename 私钥保存路径,相对于classpath
     * @return 私钥对象
     * @throws Exception
     */
    public static PrivateKey getPrivateKey(String filename) throws Exception {
        byte[] bytes = readFile(filename);
        return getPrivateKey(bytes);
    }

    /**
     * 获取公钥
     *
     * @param bytes 公钥的字节形式
     * @return
     * @throws Exception
     */
    public static PublicKey getPublicKey(byte[] bytes) throws Exception {
        X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes);
        KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePublic(spec);
    }

    /**
     * 获取密钥
     *
     * @param bytes 私钥的字节形式
     * @return
     * @throws Exception
     */
    public static PrivateKey getPrivateKey(byte[] bytes) throws Exception {
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);
        KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePrivate(spec);
    }

    /**
     * 根据密文,生存rsa公钥和私钥,并写入指定文件
     *
     * @param publicKeyFilename  公钥文件路径
     * @param privateKeyFilename 私钥文件路径
     * @param secret             生成密钥的密文
     * @throws IOException
     * @throws NoSuchAlgorithmException
     */
    public static void generateKey(String publicKeyFilename, String privateKeyFilename, String secret) throws Exception {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        SecureRandom secureRandom = new SecureRandom(secret.getBytes());
        keyPairGenerator.initialize(1024, secureRandom);
        KeyPair keyPair = keyPairGenerator.genKeyPair();
        // 获取公钥并写出
        byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
        writeFile(publicKeyFilename, publicKeyBytes);
        // 获取私钥并写出
        byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
        writeFile(privateKeyFilename, privateKeyBytes);
    }

    private static byte[] readFile(String fileName) throws Exception {
        return Files.readAllBytes(new File(fileName).toPath());
    }

    private static void writeFile(String destPath, byte[] bytes) throws IOException {
        File dest = new File(destPath);
        if (!dest.exists()) {
            dest.createNewFile();
        }
        Files.write(dest.toPath(), bytes);
    }
}

SpringSecurity登录处理的配置

响应状态枚举

/**
 * 响应状态枚举
 */
public enum ResponseStatus {
    /**
     * 内置状态
     */
    OK(200,"操作成功"),
    INTERNAL_ERROR(500000,"系统错误"),
    BUSINESS_ERROR(500001,"业务错误"),
    LOGIN_ERROR(500002,"账号或密码错误"),
    NO_DATA_ERROR(500003,"没有找到数据"),
    PARAM_ERROR(500004,"参数格式错误"),
    AUTH_ERROR(401,"没有权限,需要登录");

    private Integer code;
    private String message;

    public Integer getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }

    ResponseStatus(Integer status, String message) {
        this.code = status;
        this.message = message;
    }
}

响应数据封装对象

/**
 * 响应数据封装对象
 */
@Data
@ApiModel(value = "ResponseResult对象", description = "前端数据封装对象")
public class ResponseResult<T> {

    /**
     * 状态信息
     */
    @ApiModelProperty("响应状态")
    private ResponseStatus status;

    /**
     * 数据
     */
    @ApiModelProperty("数据")
    private T data;

    public ResponseResult(ResponseStatus status, T data) {
        this.status = status;
        this.data = data;
    }

    /**
     * 返回成功对象
     * @param data
     * @return
     */
    public static <T>  ResponseResult<T> ok(T data){
        return new ResponseResult<>(ResponseStatus.OK, data);
    }

    /**
     * 返回错误对象
     * @param status
     * @return
     */
    public static ResponseResult<String> error(ResponseStatus status){
        return new ResponseResult<>(status,status.getMessage());
    }

    /**
     * 返回错误对象
     * @param status
     * @return
     */
    public static ResponseResult<String> error(ResponseStatus status,String msg){
        return new ResponseResult<>(status,msg);
    }

    /**
     * 向流中输出结果
     * @param resp
     * @param result
     * @throws IOException
     */
    public static void write(HttpServletResponse resp, ResponseResult result) throws IOException {
        resp.setContentType("application/json;charset=UTF-8");
        String msg = new ObjectMapper().writeValueAsString(result);
        resp.getWriter().print(msg);
        resp.getWriter().close();
    }
}

用户信息

@Data
public class UserVO {

    private String username;
    private String token;
}

验证成功处理器

/**
 * 登录成功处理器
 */
@Slf4j
public class LoginSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        //获得用户名
        User user = (User) authentication.getPrincipal();
        //将用户名生成jwt token
        String token = JwtUtil.generateToken(user.getUsername(), RsaUtil.privateKey, JwtUtil.EXPIRE_MINUTES);
        //将token 发送给前端
        UserVO userVo = new UserVO(user.getUsername(),token);
        ResponseResult.write(response,ResponseResult.ok(userVo));
        log.info("user:{}  token:{}",user.getUsername() , token);
    }
}

SpringSecucrity相关配置

/**
 * SpringSecucrity相关配置
 */
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    /**
     * 密码加密器
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }

    /**
     * 设置自定义登录逻辑
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService);
    }

    /**
     * 页面资源的授权
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //对请求进行授权
        http
                .authorizeRequests()
                .antMatchers(
                        "/swagger-ui.html","/v2/**","/swagger-resources/**","/webjars/springfox-swagger-ui/**", //放行swagger相关
                        "/js/**","/css/**", //放行静态资源
                        "/login","logout"  //放行登录和登出
                 )
                .permitAll()
                .anyRequest()
                .authenticated()  //其它的请求需要登录
                .and()
                .formLogin()//登录配置
                .successHandler(new LoginSuccessHandler()) //登录成功处理
                .failureHandler((req,resp,auth) -> {//进行登录失败的处理
                    ResponseResult.write(resp,ResponseResult.error(ResponseStatus.LOGIN_ERROR));
                })
                .and()
                .exceptionHandling() //未进行登录请求的处理
                .authenticationEntryPoint((req,resp,auth)->{
                    ResponseResult.write(resp,ResponseResult.error(ResponseStatus.AUTH_ERROR));
                })
                .and()
                .logout()//登出配置
                .logoutSuccessHandler((req,resp,auth) -> {
                    ResponseResult.write(resp,ResponseResult.ok("注销成功"));
                })
                .clearAuthentication(true)
                .and()
                .cors() //跨域配置
                .configurationSource(corsConfigurationSource())
                .and()
                .csrf()
                .disable() //关闭CSRF防御
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS) //不使用session
                .and()
                .addFilter(new TokenAuthenticationFilter(authenticationManager())); //添加token验证过滤器
    }


    /**
     * 跨域配置对象
     * @return
     */
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        //配置允许访问的服务器域名
        configuration.setAllowedOrigins(Arrays.asList("*"));
        configuration.setAllowedMethods(Arrays.asList("GET","POST","PUT","DELETE"));
        configuration.setAllowedHeaders(Arrays.asList("*"));
        configuration.setAllowCredentials(true);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

后端过滤器的Token解析

@Slf4j
public class TokenAuthenticationFilter extends BasicAuthenticationFilter {

    public static final String HEADER = "Authorization";

    public TokenAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        //获得前端请求中的token
        String token = request.getHeader(HEADER);
        if(StringUtils.isBlank(token)){
            token = request.getParameter(HEADER);
        }
        //如果token为空,放行,验证失败
        if(StringUtils.isBlank(token)){
            chain.doFilter(request,response);
            return;
        }
        try {
            //解析token
            String username = JwtUtil.getUsernameFromToken(token, RsaUtil.publicKey);
            if (StringUtils.isNotBlank(username)) {
                //把用户token放入SecurityContext,通过验证
                List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("");
                User user = new User(username, "", authorities);
                UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user, null, authorities);
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            }
        }catch (ExpiredJwtException e){
            log.error("token过期",e);
        }catch (Exception ex){
            log.error("token解析错误",ex);
        }
        chain.doFilter(request,response);
    }
}

前端拦截器处理

handleLogin() {
    if (!this.username || !this.password) {
        return this.$msg.warning('用户名和密码不能为空')
    }
    let user = this.qs.stringify({username: this.username, password: this.password})
    console.log(user)
    this.axios.post('/login', user).then(result => {
        console.log(result)
        if (result.data.status === 'OK') {
            this.$msg.success('登录成功')
            localStorage.setItem('username',result.data.data.username)
            localStorage.setItem('token',result.data.data.token)
            this.$router.push({path: '/admin'})
        } else {
            this.$msg.error(result.data.data)
        }
    })
}

main.js

//错误响应拦截
axios.interceptors.response.use(res => {
  console.log('拦截响应');
  console.log(res);
  if( res.data.status === 'OK'){
    return res;
  }
  if( res.data.data === '没有权限,需要登录' ){
    MessageBox.alert('没有权限,需要登录','权限错误',{
      confirmButtonText:'跳转登录页面',
      callback: action => {
        window.location.href = '/'
      }
    })
  }else{
    Message.error(res.data.data)
  }
})

//配置axios拦截请求,添加token头信息
axios.interceptors.request.use(
  config => {
    let token = localStorage.getItem("token");
    console.log("token:" + token);
    if (token) {
      //把localStorage的token放在Authorization里
      config.headers.Authorization = token;
    }
    return config;
  },
  function(err) {
    console.log("失败信息" + err);
  }
);

在这里插入图片描述

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到