简介
通过JWT每次请求带上token
可以进行无状态登录,不必保存Session
等。
创建
在IDEA中使用Spring Initializr
创建一个Security
项目,注意,创建之后maven是不可用的,还需要添加maven
支持,模块右键,Add Framework Support
,选择maven
。
依赖
添加JWT依赖,和一个web依赖包,毕竟要用来登录认证。
<!--jwt-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
准备
提前准备好一些实体类,和一些常量。
User
实体类,实现了Spring Security
的UserDetails
接口,其实没用到这些方法,不实现也可以。
@Entity
public class User implements UserDetails {
private String username;
private String password;
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
一些常量,SECRET token
签名用的,以及token
过期时间等。
public class SecurityConstants {
public static final String SECRET = "SecretKeyToGenJWTs";
public static final long EXPIRATION_TIME = 864_000_000; // 10 days
public static final String TOKEN_PREFIX = "Bearer ";
public static final String HEADER_STRING = "Authorization";
public static final String APPLICATION_JSON_VALUE = "application/json";
}
配置自定义的用户存储认证
这种方式更为灵活,更适合在生产环境使用。这种方式不在局限于存储环境。自定义的方式也很简单。只需要提供一个UserDetailService
接口实现即可。
这里没有做任何校验可以在这里去获取数据库存储的用户进行校验,我为了测试方便,就认为验证通过了,并且获取到了密码,当然不是存储的明文密码,所以使用BCryptPasswordEncoder
进行编码。
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) {
//做校验这里
// TODO: 2020/12/23
return new User(username, new BCryptPasswordEncoder().encode("12345"), Collections.emptyList());
}
}
Controller
在Controller
增加两个接口,/login
在Security
中放行,然后用携带的用户免密去我们自定义的JWTFilter
中认证获取token
返回给前端,前端拿到token
之后就可以在每次请求时携带token进行访问,不会被拦截(这里/login
为了方便查看日志,实际上SpringSecurity提供默认的登录页面)。
@RestController
@Slf4j
public class IndexController {
@RequestMapping(value = "/login",method = RequestMethod.POST)
public boolean login(@RequestBody String raw) {
log.info("接口调用:{}",raw);
return true;
}
@RequestMapping(value = "/auth")
public String auth(@RequestBody User user) {
log.info("User,name:{},value{}",user.getUsername(),user.getPassword());
return "success";
}
}
配置JWTFilter
首先配置JWTAuthenticationFilter
认证Filter
,我们创建的身份验证过滤器扩展了UsernamepasswordauthenticationFilter
类。当我们向Spring Security
添加新过滤器时,我们可以明确定义我们希望该过滤器的过滤器链中的位置,或者我们可以让框架自行删除它。通过扩展安全框架内提供的过滤器,Spring
可以自动识别将其放在安全链中的最佳位置。
我们的自定义身份验证过滤器覆盖了基类的两种方法:
attemptAuthentication
:解析用户的凭据并将它们发出到AuthenticationManager。
successfulAuthentication
:成功登录时调用。我们使用此方法为此用户生成token。1
@Slf4j
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
/*解析用户使用manager进行认证*/
@Override
public Authentication attemptAuthentication(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response) throws AuthenticationException {
try {
User user = new ObjectMapper().readValue(request.getInputStream(), User.class);
log.info("username:{},pwd:{}",user.getUsername(),user.getPassword());
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), new ArrayList<>()));
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException();
}
}
/*当成功时调用*/
@Override
protected void successfulAuthentication(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response, javax.servlet.FilterChain chain, Authentication authResult) throws IOException, ServletException, ServletException {
String token = Jwts.builder()
.setSubject(((UserDetails) authResult.getPrincipal()).getUsername())//用户名
.setExpiration(new Date(System.currentTimeMillis() +EXPIRATION_TIME))//过期时间
.signWith(SignatureAlgorithm.HS256, SECRET)//签名
.compact();
log.info("token:{}", token);
Map<String, String> res = new HashMap<>();
res.put(HEADER_STRING, TOKEN_PREFIX + token);
response.getWriter().println(new ObjectMapper().writeValueAsString(res));
}
}
AuthenticationManager
是一个用来处理认证(Authentication)
请求的接口。在其中只定义了一个方法authenticate()
,该方法只接收一个代表认证请求的Authentication
对象作为参数,如果认证成功,则会返回一个封装了当前用户权限等信息的Authentication
对象进行返回。
Authentication authenticate(Authentication authentication) throws AuthenticationException;
在Spring Security
中,AuthenticationManager
的默认实现是ProviderManager
,而且它不直接自己处理认证请求,而是委托给其所配置的AuthenticationProvider
列表,然后会依次使用每一个AuthenticationProvider
进行认证,如果有一个AuthenticationProvider
认证后的结果不为null
,则表示该AuthenticationProvider
已经认证成功,之后的AuthenticationProvider
将不再继续认证。然后直接以该AuthenticationProvider
的认证结果作为ProviderManager
的认证结果。如果所有的AuthenticationProvider
的认证结果都为null
,则表示认证失败,将抛出一个ProviderNotFoundException
。
校验认证请求最常用的方法是根据请求的用户名加载对应的UserDetails
,然后比对UserDetails
的密码与认证请求的密码是否一致,一致则表示认证通过。
Spring Security
内部的DaoAuthenticationProvider
就是使用的这种方式。其内部使用UserDetailsService
来负责加载UserDetails
。在认证成功以后会使用加载的UserDetails来封装要返回的Authentication
对象,加载的UserDetails
对象是包含用户权限等信息的。认证成功返回的Authentication
对象将会保存在当前的SecurityContext
中。一个具有注脚的文本。2
JWTAuthorizationFilter
授权Filter
配置。
这个类作用是授权
@Slf4j
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {
public JWTAuthorizationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
/*接收token*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String header = request.getHeader(HEADER_STRING);
if (null == header || !header.startsWith(TOKEN_PREFIX)) {
chain.doFilter(request, response);
return;
}
UsernamePasswordAuthenticationToken authenticationToken = getAuthentication(request);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
chain.doFilter(request,response);
}
/*解析token授权*/
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
String token = request.getHeader(HEADER_STRING);
if (null != token) {
String user = Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token.replace(TOKEN_PREFIX, ""))
.getBody()
.getSubject();
if (null != user) {
return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
}
}
return null;
}
}
我们通过扩展BasicAuthenticationFilter
,用自定义实现将其替换,最重要部分是getAuthentication
方法。此方法从Header
读取token
,然后使用JWT验证令牌。如果一切顺利,我们将用户设置为SecurityContext
并允许请求进入下一个链。
整合Filter到SpringBoot
现在万事俱备,我们只需要将刚刚创建的Filter配置到SpringBoot中。
注意这里的注解,决定了是否启用Security,三个配置类,
- 分别配置了用户自定义认证以及加密工具;
- 资源放行;
- 权限配置。
@Configuration //开启配置
@EnableWebSecurity //开启Security配置
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsServiceImpl userDetailsService;//自定义用户认证(校验用法是否合法)
/*加密工具*/
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/*加密*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
/*放过资源,不经过安全校验*/
@Override
public void configure(WebSecurity web) {
web
.ignoring().antMatchers("/health");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors().and()
.csrf().disable()
.authorizeRequests()//开始配置权限
.antMatchers("/login")//放过他用于验证登录
.permitAll()//任何人
.anyRequest().authenticated()//所有请求都要经过授权
.and()
.addFilter(new JWTAuthenticationFilter(authenticationManager()))//自定义Filter
.addFilter(new JWTAuthorizationFilter(authenticationManager()))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
config
中权限配置比较多,这里全局放行了/health
,权限配置放行了/login
,还有一个/auth需要使用token
才能访问。下面进行验证。
验证/health
验证/login
正确密码:
错误密码:
失败重定向了,这里没有配置失败重定向。
验证/auth
带token
不带token
失败重定向。
总结
SpringBoot Security+JWT使用还是挺简单的,但是原理和配置有些复杂,还没有仔细研究,后面再研一下。