SpringSecurity在前后端分离中的应用

本文介绍了如何在前后端分离的Spring Boot项目中使用Spring Security和JWT进行登录鉴权。包括配置跨域、编写JWT工具类、实现自定义登录处理、设置过滤器以及Vue前端的JWT处理。详细阐述了每个步骤的实现细节和代码示例。
摘要由CSDN通过智能技术生成

框架简介

​ Spring Security是基于Spring AOP和Servlet过滤器的安全框架,它提供全面的安全性解决方案。同时在web请求级和方法调用级处理身份确认和授权。

项目介绍

​ 在不分离情况下使用过了SpringSecurity,感觉还是非常不错的,所以想在前端后端分离的情况下,玩一把SpringSecurity,其中遇到了不少问题,不过在度娘的支持下成功了。特此写了这篇随笔,如果文中存在一些问题和有更好的写法请告诉我,小僧万分感谢。

​ 项目用到的技术:SpringBoot +SpringSecurity +vue

具体实现

加入依赖

​ 这里就直接把我的pom贴出来了,如果其中有你用不到的依赖可以删除

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.4.5</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.zzgk</groupId>
	<artifactId>mas</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>sz-java</name>
	<description>mas project for Spring Boot</description>
	<properties>
		<java.version>1.8</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-rest</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-jdbc</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt</artifactId>
			<version>0.9.0</version>
		</dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.46</version>
        </dependency>
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>druid</artifactId>
			<version>1.1.6</version>
		</dependency>
    </dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<excludes>
						<exclude>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
						</exclude>
					</excludes>
				</configuration>
			</plugin>
		</plugins>
	</build>

</project>

jwt配置

​ 前后端分离项目,在登录时session是用不了的,所以要使用jwt(不知道jwt可以百度一下)来完成登录鉴权。

配置私钥,过期时间,名称

​ 在application.yml加入私钥,token过期时间,名称配置,refreshToken的过期时间,这里需要解释一下refreshToken是当token过期时,通过请求来刷新token的,当refreshToken过期则判断用户不活跃,这时则需要重新登录。

jwt:
  secret: ffjaksdfjak #私钥 自己可以自由发挥
  expiration: 3600000 #过期时间 半个小时
  header:  JWTHeaderName #自己往请求头部放的名字
  refresh_expiration: 651000000 #刷新token的过期时间7天
编写jwt工具类

​ 配置完成后,需要编写一个类,用于生成jwt,验证令牌,判断jwt是否过期等,

package com.zzgk.sys.util;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Data
@Component
@ConfigurationProperties(prefix = "jwt")
public class JsonWebTokenUtil {
    private String secret;
    private Long expiration;
    private String header;
    private Long refresh_expiration;

    /**
     * 生成令牌
     * @return
     */
    public String generateToken(UserDetails details,String roles){
        Map<String,Object> claims = new HashMap<>();
        claims.put("sub", details.getUsername());
        claims.put("create",new Date());
        claims.put("role",roles);
        return generateToken(claims);
    }
    /**
     * 从claims生成令牌
     * @param claims
     * @return
     */
    private String generateToken(Map<String,Object> claims){
        Date expirationDate=new Date(System.currentTimeMillis()+expiration);
        return Jwts.builder().setClaims(claims)
                .setExpiration(expirationDate)//设置过期时间
                .signWith(SignatureAlgorithm.HS512,secret)
                .compact();
    }

    /**
     *
     * @param details 用户信息
     * @param roles 用户角色
     * @return 生成RefreshToken
     */
    public String generateRefreshToken(UserDetails details, String roles){
        Map<String,Object> claims = new HashMap<>();
        claims.put("sub", details.getUsername());
        claims.put("create",new Date());
        claims.put("role",roles);
        //设置过期时间为七天;
        Date expirationDate = new Date(System.currentTimeMillis()+refresh_expiration);
        return Jwts.builder().setClaims(claims)
                .setExpiration(expirationDate)
                .signWith(SignatureAlgorithm.HS512,secret)
                .compact();
    }

    /**
     * 从令牌中获取用户名
     * @param token 令牌
     * @return 用户名
     */
    public  String getUsernameFromToken(String token){
        String username;
        try {
            Claims claims=getClaimsFromToken(token);
            username=claims.getSubject();
        }catch (ExpiredJwtException e){
            username=null;
        }
        return username;
    }

    /**
     * 无视过期时间,获取用户名
     * @param token
     * @return
     */
    public  String getUsernameIgnoreExpiration(String token){
        String username;
        try {
            Claims claims=getClaimsFromToken(token);
            username=claims.getSubject();
        }catch (ExpiredJwtException e){
            username=e.getClaims().getSubject();
        }
        return username;
    }
    /**
     * 从令牌中获取数据声明,
     * @param token 令牌
     * @return 数据声明
     */
    private Claims getClaimsFromToken(String token){
        return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
    }

    /**
     * 判断令牌是否过期
     *
     * @param token 令牌
     * @return 是否过期
     */
    private Boolean isTokenExpired(String token){
        try{
            Claims claims=getClaimsFromToken(token);
            Date expiration=claims.getExpiration();
            return expiration.before(new Date());
        }catch (Exception e){
            return  false;
        }
    }

    /**
     * 刷新令牌
     *
     * @param token 原令牌
     * @return 新令牌
     */
    public String refreshToken(String token){
        String refreshedToken;
        try {
            Claims claims=getClaimsFromToken(token);
            claims.put("created",new Date());
            refreshedToken=generateToken(claims);
        }catch (ExpiredJwtException e){
            Claims claims = e.getClaims();
            claims.put("created",new Date());
            refreshedToken= generateToken(claims);
        }
        return  refreshedToken;
    }

    /**
     * 验证令牌
     * @param token 令牌
     * @param userDetails 用户
     * @return 是否有效
     */
    public Boolean validateToken(String token,UserDetails userDetails){
        String username=getUsernameFromToken(token);
        return (username.equals(userDetails.getUsername())&&!isTokenExpired(token));
    }
}

Spring Security配置

一、起步

​ 编写一个类(我起名为MasSecurity) 让这个类继承WebSecurityConfigurerAdapter,同时添加@EnableWebSecurity注解,重写configure(AuthenticationManagerBuilder auth)和 configure(HttpSecurity http)方法

@EnableWebSecurity
public class MasSecurity extends WebSecurityConfigurerAdapter{
     @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
      …… 
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
      …… 
    }
}

​ 这一块的配置还是蛮多的,在配置的时候也花了不少时间。因为项目是前后端分离的,这样就涉及到了跨域问题,首先我们先来解决跨域问题。

二、跨域配置

​ 在configure(HttpSecurity http)方法中添加如下配置,允许跨域后SpringSecurity会自动寻找名字为corsConfigurationSource的Bean,使用该bean的配置

@Override
protected void configure(HttpSecurity http) throws Exception {
	http.cors();//允许跨域,配置后SpringSecurity会自动寻找name=corsConfigurationSource的Bean
	http.csrf().disable();//关闭CSRF防御
}

​ 那么接下来,我们只需要写一个corsConfigurationSource的bean,来设置跨域相关的配置

package com.zzgk.sys.config;

import com.zzgk.sys.util.JsonWebTokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
 * 跨域配置
 * @author cpms
 */
@Configuration
public class CrossDomainConfig {

    @Autowired
    JsonWebTokenUtil jsonWebTokenUtil;

    /**
     *
     * @return 基于URL的跨域配置信息
     */
    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration cores=new CorsConfiguration();
        cores.setAllowCredentials(true);//允许客户端携带认证信息
        //springBoot 2.4.1版本之后,不可以用 * 号设置允许的Origin,如果不降低版本,则在跨域设置时使用setAllowedOriginPatterns方法
       // cores.setAllowedOrigins(Collections.singletonList("*"));//允许所有域名可以跨域访问
        cores.setAllowedOriginPatterns(Collections.singletonList("*"));
        cores.setAllowedMethods(Arrays.asList("GET","POST","DELETE","PUT","UPDATE"));//允许哪些请求方式可以访问
        cores.setAllowedHeaders(Collections.singletonList("*"));//允许服务端访问的客户端请求头
        // 暴露哪些头部信息(因为跨域访问默认不能获取全部头部信息)
        cores.addExposedHeader(jsonWebTokenUtil.getHeader());
        // 注册跨域配置
        // 也可以使用CorsConfiguration 类的 applyPermitDefaultValues()方法使用默认配置
        source.registerCorsConfiguration("/**",cores.applyPermitDefaultValues());
        return source;
    }

}

这样跨域配置设置完毕

三、登录配置

​ 登录的配置还是比较多的,首先我们要写一个service让其实现UserDetailsService接口,重写loadUserByUsername方法

@Service
public class UserServiceImpl implements UserDetailsService {
	@Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        UserEntity user = userDao.findByUserName(s);
        if (user!=null){
            SysUserDetail detail = new SysUserDetail();
            detail.setId(user.getId());
            detail.setUsername(user.getUsername());
            detail.setPassword(user.getPassword());
            AccountState accountState = accountStateDao.getAccountSateByUserid(user.getId());
            detail.setAccountNonExpired(accountState.getAccountNonExpired() == 1);
            detail.setAccountNonLocked(accountState.getAccountNonLocked()==1);
            detail.setEnabled(accountState.getEnabled()==1);
            detail.setCredentialsNonExpired(accountState.getCredentialsNonExpired()==1);
            //查询用户权限
            List<GrantedAuthority> authorities =new ArrayList<>();
            List<Map<String,Object>> roles=roleDao.getRoleList(user.getId());
            for (Map<String,Object> one:roles) {
                SimpleGrantedAuthority authority=new SimpleGrantedAuthority((String) one.get("code"));
                authorities.add(authority);
            }
            detail.setAuthorities(authorities);
            return detail;
        }else
        throw new UsernameNotFoundException("该账号不存在");
    }
}

​ 实现一个自定义的登录处理,编写一个类实现AuthenticationProvider接口

package com.zzgk.sys.config;
import com.zzgk.sys.serviceimpl.sys.UserServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;
/**
 * 登录处理,
 */
@Component
public class LoginAuthProvider implements AuthenticationProvider {

    @Autowired
    UserServiceImpl userServiceImpl;

    @Override
    public Authentication authenticate(Authentication auth) throws AuthenticationException {
        //获取用户名和密码
        String username = auth.getName();
        String password = (String) auth.getCredentials();
        UserDetails userDetail  = userServiceImpl.loadUserByUsername(username);
        if (!userDetail.isEnabled()){
            throw new DisabledException("该账号已禁用,请联系管理员");
        }else if (!userDetail.isAccountNonExpired()){
            throw new AccountExpiredException("该账号已过期,请联系管理员");
        }else if(!userDetail.isAccountNonLocked()){
            throw new LockedException("该账号已被锁定,请联系管理员");
        }else if(!userDetail.isCredentialsNonExpired()){
            throw new CredentialsExpiredException("该账号的登录凭证已过期,请重新登录");
        }
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        if(!passwordEncoder.matches(password,userDetail.getPassword())){
            throw  new BadCredentialsException("密码错误请重新输入");
        }
        return new UsernamePasswordAuthenticationToken(userDetail,password,userDetail.getAuthorities());
    }

    // supports函数用来指明该Provider是否适用于该类型的认证,如果不合适,则寻找另一个Provider进行验证处理。
    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

​ 将自定义处理加入Spring Security中,在configure(AuthenticationManagerBuilder auth)中加入该方法。

@EnableWebSecurity
public class MasSecurity extends WebSecurityConfigurerAdapter {

    @Autowired
    LoginAuthProvider loginAuthProvider; 	

	@Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //登录处理
        auth.authenticationProvider(loginAuthProvider);
    }
}    

​ 继续在configure(HttpSecurity http)方法中添加一些配置。

protected void configure(HttpSecurity http) throws Exception {
        //允许跨域,配置后SpringSecurity会自动寻找name=corsConfigurationSource的Bean
        http.cors();
        http.csrf().disable();
        //当访问接口失败的配置
        http.exceptionHandling().authenticationEntryPoint(new InterfaceAccessException());
        http.authorizeRequests()
                .antMatchers("/login","/refreshToken","/user/check_login").permitAll()//设置哪些方法允许匿名访问
                .antMatchers("/admin/*").hasAnyRole("ROLE_ADMIN")//该方法需要管理员角色才能访问
                .anyRequest().authenticated()//其他方法都需要登录才能访问
                .and()
                .formLogin()//选用formLogin模式
                .successHandler(new MySuccessHandler())//登录成功的处理
                .failureHandler(new MyFailHandler())//登录失败的处理
                .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);//因为用不到session,所以选择禁用
        //向过滤器链中添加,自定义的jwt过滤器和json过滤器
        //在UsernamePasswordAuthenticationFilter之前添加jwtAuthenticationFilter
        http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
        //在UsernamePasswordAuthenticationFilter之后添加jsonAuthenticationFilter
            .addFilterAfter(getJsonAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class);
    }

​ 登录成功的处理类

	//登录成功的处理类
     class MySuccessHandler implements AuthenticationSuccessHandler{

        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                            Authentication authentication) throws IOException, ServletException {
            UserDetails details = (UserDetails) authentication.getPrincipal();
            List<GrantedAuthority> roles = (List<GrantedAuthority>) details.getAuthorities();
            //登录时同时生成refreshToken,保存到表中
            RefreshToken token = new RefreshToken();
            token.setUsername(details.getUsername());
            String refreshToken = jsonWebTokenUtil.generateRefreshToken(details,roles.get(0).getAuthority());
            token.setToken(refreshToken);
            //如果存在则更新
            if (refreshTokenDao.existRefreshToken(details.getUsername())>0){
                refreshTokenDao.updateRefreshToken(token);
            }else {
                refreshTokenDao.save(token);
            }
            //将jwt返回
            response.setHeader(jsonWebTokenUtil.getHeader(), jsonWebTokenUtil.generateToken(details,roles.get(0).getAuthority()));
            //返回前端
            ResponseMsgUtil.sendSuccessMsg("成功",null,response);

        }
    }

​ 登录失败的处理类

//登录失败的处理
    static class MyFailHandler implements AuthenticationFailureHandler{

        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                            AuthenticationException e) throws IOException, ServletException {
          	//获取错误信息返回前台
            ResponseMsgUtil.sendFailMsg(e.getMessage(),response);
        }
    }

jwtAuthenticationFilter类,该类主要作用于从前端请求头部解析出jwt,检查是否登录。

package com.zzgk.sys.config;

import com.zzgk.sys.serviceimpl.sys.UserServiceImpl;
import com.zzgk.sys.util.JsonWebTokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 登录成功后,此类进行鉴权,
 */
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    JsonWebTokenUtil tokenUtil;
    @Autowired
    UserServiceImpl userServiceImpl;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        //从请求头部获取json web token
        String jwt = request.getHeader(tokenUtil.getHeader());
        //判断是否不为空
        if (StringUtils.hasLength(jwt)&&!jwt.equals("null")&&!jwt.equals("undefined")) {
            //从jwt中获取用户名
            String username = tokenUtil.getUsernameFromToken(jwt);
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {

                //通过用户名查询
                UserDetails userDetails = userServiceImpl.loadUserByUsername(username);
                //创建认证信息
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username,
                        userDetails.getPassword(), userDetails.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
        filterChain.doFilter(request,response);
    }
}

​ getJsonAuthenticationFilter()方法

 @Bean
    JsonAuthenticationFilter getJsonAuthenticationFilter() throws Exception {
        JsonAuthenticationFilter filter = new JsonAuthenticationFilter();
        filter.setAuthenticationManager(authenticationManagerBean());
        return filter;
    }

JsonAuthenticationFilter类,实现了从json获取用户名和密码

package com.zzgk.sys.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;

/**
 * 默认的用户名/密码提取是通过request中的getParameter来提取的
 * 该过滤器实现了从json获取用户名和密码
 * @author cpms
 */
public class JsonAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {
        //判断请求类型是否是json
        if (request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE) || request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)){
            ObjectMapper mapper = new ObjectMapper();
            UsernamePasswordAuthenticationToken authenticationToken = null;
            try {
                InputStream is = request.getInputStream();
                Map<String,String> authenticationBean = mapper.readValue(is,Map.class);
                authenticationToken = new UsernamePasswordAuthenticationToken(authenticationBean.get("username"),
                        authenticationBean.get("password"));
            }catch (IOException e){
                e.printStackTrace();
                authenticationToken = new UsernamePasswordAuthenticationToken("","");
            }
            setDetails(request,authenticationToken);
            return this.getAuthenticationManager().authenticate(authenticationToken);
        }else {
            return super.attemptAuthentication(request, response);
        }

    }
}

​ InterfaceAccessException类,用于处理接口访问时身份过期

package com.zzgk.sys.config;

import com.zzgk.sys.util.ResponseMsgUtil;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class InterfaceAccessException implements AuthenticationEntryPoint {



    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {

        if(e.getMessage().equals("Full authentication is required to access this resource")){
            ResponseMsgUtil.send401Msg("登录超时,请重新登录",response);
        }else {
            ResponseMsgUtil.sendFailMsg(e.getMessage(),response);
        }
    }

    public boolean isAjaxRequest(HttpServletRequest request) {
        String ajaxFlag = request.getHeader("X-Requested-With");
        return ajaxFlag != null && "XMLHttpRequest".equals(ajaxFlag);
    }
}

​ MasSecurity的全部配置

package com.zzgk.sys.config;
import com.zzgk.sys.dao.sys.RefreshTokenDao;
import com.zzgk.sys.entity.sys.RefreshToken;
import com.zzgk.sys.util.JsonWebTokenUtil;
import com.zzgk.sys.util.ResponseMsgUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;


/**
 * spring security 配置类
 * @author cpms
 */
@EnableWebSecurity
public class MasSecurity extends WebSecurityConfigurerAdapter {

    @Autowired
    LoginAuthProvider loginAuthProvider;
    @Autowired
    JwtAuthenticationFilter jwtAuthenticationFilter;
    @Autowired
    JsonWebTokenUtil jsonWebTokenUtil;
    @Autowired
    RefreshTokenDao refreshTokenDao;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //登录处理
        auth.authenticationProvider(loginAuthProvider);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //允许跨域,配置后SpringSecurity会自动寻找name=corsConfigurationSource的Bean
        http.cors();
        http.csrf().disable();
        //当访问接口失败的配置
        http.exceptionHandling().authenticationEntryPoint(new InterfaceAccessException());
        http.authorizeRequests()
                .antMatchers("/login","/refreshToken","/user/check_login").permitAll()
                .antMatchers("/admin/*").hasAnyRole("ROLE_ADMIN")
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .successHandler(new MySuccessHandler())//登录成功的处理
                .failureHandler(new MyFailHandler())//登录失败的处理
                .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);//因为用不到session,所以选择禁用
        //向过滤器链中添加,自定义的jwt过滤器和json过滤器
        //在UsernamePasswordAuthenticationFilter之前添加jwtAuthenticationFilter
        http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
        //在UsernamePasswordAuthenticationFilter之后添加jsonAuthenticationFilter
            .addFilterAfter(getJsonAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class);
    }

    //登录成功的处理类
     class MySuccessHandler implements AuthenticationSuccessHandler{

        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                            Authentication authentication) throws IOException, ServletException {

            UserDetails details = (UserDetails) authentication.getPrincipal();
            List<GrantedAuthority> roles = (List<GrantedAuthority>) details.getAuthorities();
            //登录时同时生成refreshToken,保存到表中
            RefreshToken token = new RefreshToken();
            token.setUsername(details.getUsername());
            String refreshToken = jsonWebTokenUtil.generateRefreshToken(details,roles.get(0).getAuthority());
            token.setToken(refreshToken);
            if (refreshTokenDao.existRefreshToken(details.getUsername())>0){
                refreshTokenDao.updateRefreshToken(token);
            }else {
                refreshTokenDao.save(token);
            }
            response.setHeader(jsonWebTokenUtil.getHeader(), jsonWebTokenUtil.generateToken(details,roles.get(0).getAuthority()));
            ResponseMsgUtil.sendSuccessMsg("成功",null,response);

        }
    }

    //登录失败的处理
    static class MyFailHandler implements AuthenticationFailureHandler{

        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                            AuthenticationException e) throws IOException, ServletException {
           ResponseMsgUtil.sendFailMsg(e.getMessage(),response);
        }
    }

    @Bean
    JsonAuthenticationFilter getJsonAuthenticationFilter() throws Exception {
        JsonAuthenticationFilter filter = new JsonAuthenticationFilter();
        filter.setAuthenticationManager(authenticationManagerBean());
        return filter;
    }

}

vue配置

保存jwt
 localStorage.setItem("jwt",res.headers.jwtheadername);
axios配置
import axios from 'axios'
import app from '../main'
import store from '../vuex/store'
import qs from 'qs'

//获取jwt
function getLocalToken() {
    const token = localStorage.getItem("jwt");
    return token;
}

//保存jwt
function saveToken(obj) {
    localStorage.setItem("jwt", obj);
}
function clearToken(){
    localStorage.removeItem("jwt");
}
//刷新jwt
function refreshToken() {
    return axios.post("/refreshToken");
}
//验证jwt
function validateToken() {
    return axios.post("/validateToken");
}

async function validateRefreshToken(c){
    let valid = await validateToken().then(res=>{
        return res.data.state=='ok' 
    });
    if(!valid){
        if (!isRefreshing) {
            config.log("刷新token中");
            isRefreshing = true;
            refreshToken().then(res => {
                //刷新失败
                if (res.data.state == 'fail') {
                    //如果失败就清空队列,防止多次接口无效访问。
                    requests=[];
                    throw res.data.msg;
                }else{
                    const jwt = res.data.data.jwt;
                    saveToken(jwt);
                    isRefreshing = false;
                    return token;
                }
            }).then((token) => {
                requests.forEach(cb => cb(token));
                //执行完成后,清空队列
                requests = [];
            }).catch(error => {
                console.log(error);
            });
        }
        const retryOriginalRequst = new Promise((resolve) => {
            requests.push((token) => {
                config.headers['JWTHeaderName'] = token;
                resolve(config);
            })
        })
        return retryOriginalRequest;
    }
    return c;
}

// `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL。
axios.defaults.baseURL = "http://localhost:8700";


//是否正在刷新的标记
let isRefreshing = false;

//重试队列,每一项将是一个待执行的函数形式
let requests = []

//request拦截器,每次请求前从cookie中获取jwt,加入请求头部
axios.interceptors.request.use(
    config => {
        let jwt = getLocalToken();
       //console.log(localStorage);
        config.headers['JWTHeaderName'] = jwt;
        //登录接口、刷新token接口、检查登录接口直接跳过验证
        if (config.url.indexOf("/login") >= 0 || config.url.indexOf("/refreshToken") >= 0
            || config.url.indexOf("/user/check_login") >= 0 || config.url.indexOf('/validateToken') >= 0) {
            //let jwt =  store.state.jwt;
            return config;
        }
        return validateRefreshToken(config);
    },
    error => {
        Promise.reject(error);
    }
)

//response拦截器
axios.interceptors.response.use(
    response => {
        const resp = response.data;
        if(resp.code=='200'){
            return response;
        }else if(resp.code=='401'){
            app.$message("登录过期,请重新登录");
            window.location.href = '/';        
        } else{
            console.log(response)
            return Promise.reject(resp.msg)
        }
    },
    error => {
        if (error && error.response) {
            switch (error.response.status) {
                case 400:
                    error.message = '错误请求';
                    break;
                case 401:
                    error.message = '请重新登录';
                    break;
                case 403:
                    error.message = "拒绝访问";
                    break;
                case 404:
                    error.message = '请求错误,未找到该资源';
                    break;
                case 405:
                    error.message = '请求方法未允许';
                    break;
                case 408:
                    error.message = '请求超时';
                    break;
                case 500:
                    error.message = '服务器端出错';
                    break;
                case 501:
                    error.message = '网络未实现';
                    break;
                case 502:
                    error.message = '网络错误';
                    break;
                case 503:
                    error.message = '服务不可用';
                    break;
                case 504:
                    error.message = '网络超时';
                    break;
                case 505:
                    error.message = 'http版本不支持该请求';
                    break;
                default:
                    error.message = `未知错误${error.response.status}`;

            }
        } else {
            error.message = "连接到服务器失败";
        }
        return Promise.reject(error.message);
    }
)


export default {
    serverUrl: "http://localhost:8700",
    post: function (url, data, config) {
        if (!data)
            data = {};
        if (!config)
            config = {};
        config.header = {};
        if (data instanceof FormData) {
            if (!config.header)
                config.header = {};
            config.header['content-type'] = 'multipart/form-data';
            return axios.post(url, data, config);
        }
        return axios.post(url, qs.stringify(data, { arrayFormat: 'repeat' }), config);
    },
    get: function (url, config) {
        if (!config)
            config = {};
        config.withCredentials = true;
        return axios.get(url, config);
    },
    put: function (url, data, config) {
        if (!data) {
            data = {};
        }
        if (!config) {
            config = {};
        }
        config.header = {}
        return axios.put(url, qs.stringify(data, { arrayFormat: 'repeat' }), config);
    },
    delete(url, data, config){
        if (!data) {
            data = {};
        }
        if (!config) {
            config = {};
        }
        config.header = {}
        return axios.delete(url, qs.stringify(data, { arrayFormat: 'repeat' }), config)
    },
    getLocalToken,
    validateToken,
    clearToken,
}
router配置
function existToken(){
  return new Promise((resolve, reject)=>{
      let token = global.getLocalToken();
      resolve(token!=null&&token.trim().length>0);
  })
}


// 每次跳转页面前,检查是否已经登录
router.beforeEach((to, from, next) => {
  //当跳转页时login时清空标签栏
  if (to.name == "login") {
    //如果已含有jwt且没过期就直接去index
    let  exist  = existToken();
    if(exist){
      global.post("/user/check_login").then(function (res, req) {
        if (res.data.state == "ok") {
          if (res.data.data){
            localStorage.setItem("jwt", res.data.data);
          }
          next({name:'index'});
        } 
      }).catch(error => {
        console.log();
      });
    }
  }
  if (to.meta.requireAuth) {
    //const now = Date.now();
    //const exp = localStorage.getItem("exp");
    global.post("/user/check_login").then(function (res, req) {
      if (res.data.state == "ok") {
        if (res.data.data){
          localStorage.setItem("jwt", res.data.data);
        }
        next();
      } else {
        //store.dispatch('clearTab');
        app.$message(res.data.msg);
        next({ name: 'login' });
      }
    }).catch(error => {
      if (typeof error != 'undefined') {
        app.$message(error);
      }
      next({ name: 'login' });
    });
  } else {
    next()
  }

});

github地址

vue:https://github.com/FengQingZhang/sz-vue

java:https://github.com/FengQingZhang/sz-java

结语

如果您发现文章中有哪些问题,请联系我并指导我改正,或者您有更好的写法,麻烦您指点一二,本人万分感谢,
文中vue部分参考了https://zhuanlan.zhihu.com/p/80125501这表文章,对作者表示感谢。
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值