一些理解,不一定正确
请求通过API,进入Basic Authentication Filter,在这里拿到token,解析token,拿到username和password,将用户输入的用户名密码进行封装,并提供给AuthenticationManager.authenticate()方法进行验证,验证成功后,返回一个认证成功的UsernamePasswordAuthenticationToken(Authentication)对象,然后放在security的context中,继续过滤链。
如果没有token,或者token不对,则继续过滤链,到达UsernamePasswordAuthenticationFilter,在attemptAuthentication()方法中,从请求的header中,拿到user和password,调用AuthenticationManager.authenticate()进行认证
AuthenticationManager验证过程
AuthenticationManager.authenticate()接收Authentication对象作为参数,由其实现类ProviderManager完成对其进行验证
在ProviderManager的authenticate方法中,轮训成员变量List providers。该providers中如果有一个
AuthenticationProvider的supports函数返回true,那么就会调用该AuthenticationProvider的authenticate函数认证,调用SecurityConfig中指定的userService,没有则使用UserDetailsService ,调用loadUserByUsername() 方法从数据库中加载用户信息 ,对密码等进行验证,整个认证过程结束。如果不成功,则继续使用下一个合适的AuthenticationProvider进行认证,只要有一个认证成功则为认证成功。
参考大佬的验证原理
代码
spring security + JWT依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.19.2</version>
</dependency>
创建SecurityConfig类,配置springSecurity
package xyz.heguchangan.easymusicapp.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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 xyz.heguchangan.easymusicapp.exception.RestAuthenticationEntryPoint;
import xyz.heguchangan.easymusicapp.filter.JwtAuthenticationFilter;
import xyz.heguchangan.easymusicapp.filter.JwtAuthorizationFilter;
import xyz.heguchangan.easymusicapp.service.UserService;
/**
* springSecurity的配置文件
* @Configuration:声明为配置类
* @EnableWebSecurity:声明该类为springSecurity的配置类
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/** 密钥 */
public static final String SECRET = "heguchangan";
/**JWT令牌过期时间*/
public static final long EXPIRATION_TIME = 864000000;
/** TOKEN的前缀*/
public static final String TOKEN_PREFIX = "Bearer";
/** header中,token对应的变量名*/
public static final String HEADER_STRING = "Authorization";
/** 注册的入口,注册不开启JWT鉴权*/
public static final String SIGN_UP_URL = "/users/";
/** 自动装载bean */
UserService userService;
RestAuthenticationEntryPoint restAuthenticationEntryPoint;
@Autowired
public void setRestAuthenticationEntryPoint(RestAuthenticationEntryPoint restAuthenticationEntryPoint) {
this.restAuthenticationEntryPoint = restAuthenticationEntryPoint;
}
@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
/*
* 配置springSecurity
* */
@Override
protected void configure(HttpSecurity http) throws Exception {
/* .cors()开启跨域
* .csrf().disable() csrf关闭
* .authorizeRequests()开启request的鉴权
* antMatchers.permitAll(类型,地址)开启白名单,SIGN_UP_URL地址的post请求类型全部允许
* anyRequest() 剩下的所有request都需要鉴权
* addFilter 使用拦截器,这两个拦截器后面会讲
* 然后exceptionHandling对错误码进行处理 重写authenticationEntryPoint()类自定义错误页面
* 再把session设置成无状态
* */
http.cors()
.and()
.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.POST,SIGN_UP_URL).permitAll()
.anyRequest().authenticated()
.and()
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
.addFilter(new JwtAuthorizationFilter(authenticationManager()))
.exceptionHandling()
.authenticationEntryPoint(restAuthenticationEntryPoint)
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Override
public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
/** 指定service,这样通过username从数据库里返回数据时,就会使用userService里的方法*/
authenticationManagerBuilder.userDetailsService(userService);
}
}
两个过滤器
JwtAuthorizationFilter
package xyz.heguchangan.easymusicapp.filter;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import xyz.heguchangan.easymusicapp.config.SecurityConfig;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
/*
* 授权拦截器,如果当前request有token的话
* 从Header中拿到token,对token进行鉴权
* 然后把用户放到context中
* */
public class JwtAuthorizationFilter extends BasicAuthenticationFilter {
//构造函数
public JwtAuthorizationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
/** 重写方法 */
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
// 拿到header中的token
String token = request.getHeader(SecurityConfig.HEADER_STRING);
//没有header,或者token不是规定的前缀
if(token == null || !token.startsWith(SecurityConfig.TOKEN_PREFIX)){
// 不做处理,给下一个过滤器,还可以通过username和password进行鉴权
chain.doFilter(request,response);
return;
}
// 进行鉴权,拿到鉴权之后的Authentication对象
UsernamePasswordAuthenticationToken authenticationToken = getAuthentication(token);
// 把Authentication对象放在security的context中,继续过滤链
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
chain.doFilter(request,response);
}
/*
* 进行token鉴权
* */
private UsernamePasswordAuthenticationToken getAuthentication(String header){
if (header != null) {
/* HMAC512签名解码,build之后,验证(需要把前缀拿掉)拿到username */
String username = JWT.require(Algorithm.HMAC512(SecurityConfig.SECRET))
.build()
.verify(header.replace(SecurityConfig.TOKEN_PREFIX, ""))
.getSubject();
if (username != null) {
/** 如果username不为空,username鉴权*/
User user = userService.loadUserByUsername(username);
return new UsernamePasswordAuthenticationToken(username, user.getPassword(), new ArrayList<>());
}
}
return null;
}
}
JwtAuthenticationFilter
该拦截器通过username和password进行鉴权
package xyz.heguchangan.easymusicapp.filter;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.authentication.AuthenticationManager;
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 xyz.heguchangan.easymusicapp.config.SecurityConfig;
import xyz.heguchangan.easymusicapp.entity.User;
import xyz.heguchangan.easymusicapp.exception.BizException;
import xyz.heguchangan.easymusicapp.exception.ExceptionType;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
/*
* JWT鉴权拦截器,
* 该类主要处理拿到username和password
* 进行鉴权
* */
// 因为是针对username 和password 所以继承UsernamePasswordAuthenticationFilter类
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
/** 不用管 在springSecurity的config中会传来这个类 */
private final AuthenticationManager authenticationManager;
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
/** 尝试对请求进行鉴权*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
/**获取输入中的username和pwd */
//从输入流中映射json对应的属性给user对象,就是把json转换成user对象
User user = new ObjectMapper().
readValue(request.getInputStream(),User.class);
// 通过username和pwd生成token,对token鉴权返回结果
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
user.getUsername(),
user.getPassword(),
new ArrayList<>()
)
);
} catch (IOException e) {
e.printStackTrace();
/* 抛出自定义错误 */
throw new BizException(ExceptionType.FORBIDDEN);
}
}
/** 鉴权成功的处理 */
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
/** 创建JWT的token
* withSubject设置规则为username,authResult.getPrincipal()返回object需要转成user
* .withExpiresAt设置到期时间,当前时间加上存活的时间
* .sign HMAC512用设定好的字符串签名*/
String token = JWT.create()
.withSubject(((User)authResult.getPrincipal()).getUsername())
.withExpiresAt(new Date(System.currentTimeMillis()+ SecurityConfig.EXPIRATION_TIME))
.sign(Algorithm.HMAC512(SecurityConfig.SECRET));
/** 添加到Header中,name+value*/
response.addHeader(SecurityConfig.HEADER_STRING,SecurityConfig.TOKEN_PREFIX+token);
}
}
UserService
springSecurity底层需要访问数据库,需要继承UserDetailsService,实现loadUserByUsername方法
public interface UserService extends UserDetailsService {
/*通过username,从数据库拿出user信息*/
@Override
User loadUserByUsername(String username) throws UsernameNotFoundException;
}
同时 user类需要实现UserDetails
public class User implements UserDetails {
....
}