框架简介
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这表文章,对作者表示感谢。