SpringSecurity默认采用的基于表单的认证形式,以session识别 从用户登录授权、鉴权等都与表单相关。而当前许多应用都采用SpringBoot 基于Resful API风格的开发,使用默认的SpringSecurity无法直接使用满足。解决方案:不采用默认的登录鉴权方式,而通过覆写增加其中的过滤器Filter来实现 登录和鉴权,并将JWT(JSON WEB TOKEN)作为认证机制。
原理:
实现步骤:
源码地址:https://github.com/YoungerJam/SpringSecurityWithJwtDemo
// JWTLoginFilter.java
package com.SpringSecurityWithJwt.demo.filter;
import com.SpringSecurityWithJwt.demo.entity.User;
import com.SpringSecurityWithJwt.demo.utils.JwtUtils;
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.core.GrantedAuthority;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
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.Collection;
/**
* 登录过滤器 client发送POST请求到此验证
* 验证成功后将生成返回token给client
*
* @Author: Jam
* @Date: 2020/3/12 17:24
*/
public class JWTLoginFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
/**
* 将登录请求的路径设置为 /auth (默认下的 /login)
* @param authenticationManager 认证管理
*/
public JWTLoginFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
super.setFilterProcessesUrl("/auth");
}
/**
* 重点覆写的方法,方法名直接译 尝试认证
* @param request 携带 用户名+密码 的请求体
* @param response
* @return 认证信息
* @throws AuthenticationException
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
User user = new User();
user.setUsername(request.getParameter("username"));
user.setPassword(request.getParameter("password"));
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(
user.getUsername(),
user.getPassword(),
new ArrayList<>()
));
}catch (Exception e){
e.printStackTrace();
return null;
}
}
/**
* 覆写成功认证的方法,若账号密码校验成功,会调用到此方法
* 并生成对应的token返回给client
* @param request
* @param response 返回给client的响应体,(会在头部加上token认证令牌,此后的访问需携带此token)
* @param chain
* @param auth
* @throws IOException
* @throws ServletException
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,Authentication auth)throws IOException, ServletException{
Collection<? extends GrantedAuthority> authorities=auth.getAuthorities();
String username=((org.springframework.security.core.userdetails.User) auth.getPrincipal()).getUsername();
String role="";
for(GrantedAuthority authority:authorities){
role=authority.getAuthority();
}
System.out.println(username+" "+role);
String token=JwtUtils.createToken(username,role);
response.addHeader(JwtUtils.TOKEN_HEADER, JwtUtils.TOKEN_PREFIX+token);
}
}
// JWTAuthenticationFilter.java
package com.SpringSecurityWithJwt.demo.filter;
import com.SpringSecurityWithJwt.demo.utils.JwtUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
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.Collections;
/**
* token校验过滤器
* 携带token的http请求将会在此类进行token校验
* jwt提供了token检验机制
*
* @Author: Jam
* @Date: 2020/3/12 18:00
*/
public class JWTAuthenticationFilter extends BasicAuthenticationFilter {
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
public JWTAuthenticationFilter(AuthenticationManager authenticationManager, AuthenticationEntryPoint authenticationEntryPoint) {
super(authenticationManager, authenticationEntryPoint);
}
/**
* 覆写方法
* @param request 携带token的请求体
* @param response
* @param chain
* @throws IOException
* @throws ServletException
*/
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
String header=request.getHeader(JwtUtils.TOKEN_HEADER);
//过滤没有token的(可能是无需授权的访问)
if(header==null||!header.startsWith(JwtUtils.TOKEN_PREFIX)){
chain.doFilter(request,response);
return;
}
UsernamePasswordAuthenticationToken authentication = getAuthentication(header);
//设置该用户的认证信息,由jwtToken生成UsernamePasswordAuthenticationToken
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
}
private UsernamePasswordAuthenticationToken getAuthentication(String header) {
String token = header.replace(JwtUtils.TOKEN_PREFIX, "");
String username = JwtUtils.getUsername(token);
String role = JwtUtils.getUserRole(token);
System.out.println("授权:"+username+" "+role);
if (username != null) {
return new UsernamePasswordAuthenticationToken(username, null,
Collections.singleton(new SimpleGrantedAuthority(role))
);
}
return null;
}
}
// JwtUtils.java
package com.SpringSecurityWithJwt.demo.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @Author: Jam
* @Date: 2020/3/13 0:36
*/
public class JwtUtils {
public static final String TOKEN_HEADER="Authorization";
public static final String TOKEN_PREFIX="Bearer ";
private static final String SECRET="MyJwtSecret";
private static final long EXPIRATION=7200;
private static final String ROLE_CLAIMS="rol";
public static String createToken(String username,String role){
Map<String,Object> map=new HashMap<>();
map.put(ROLE_CLAIMS,role);
return Jwts.builder()
.signWith(SignatureAlgorithm.HS512,SECRET)
.setClaims(map)
.setSubject(username)
.setExpiration(new Date(System.currentTimeMillis()+EXPIRATION*1000))
.compact();
}
public static String getUsername(String token){
return getTokenBody(token).getSubject();
}
public static String getUserRole(String token){
return (String) getTokenBody(token).get(ROLE_CLAIMS);
}
//解析jwt
private static Claims getTokenBody(String token){
return Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody();
}
}
// JWTAuthenticationEntryPoint 异常处理类
package com.SpringSecurityWithJwt.demo.exception;
import com.fasterxml.jackson.databind.ObjectMapper;
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;
/**
* @Author: Jam
* @Date: 2020/3/13 15:09
*/
public class JWTAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write(new ObjectMapper().writeValueAsString("无访问权限 "+authException.getMessage()));
}
}
// SecurityConfig.java
package com.SpringSecurityWithJwt.demo.config;
import com.SpringSecurityWithJwt.demo.exception.JWTAuthenticationEntryPoint;
import com.SpringSecurityWithJwt.demo.filter.JWTAuthenticationFilter;
import com.SpringSecurityWithJwt.demo.filter.JWTLoginFilter;
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.method.configuration.EnableGlobalMethodSecurity;
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.crypto.bcrypt.BCryptPasswordEncoder;
import javax.sql.DataSource;
/**
* @Author: Jam
* @Date: 2020/3/9 16:31
*/
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final DataSource dataSource;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
@Autowired
public SecurityConfig(DataSource dataSource, BCryptPasswordEncoder bCryptPasswordEncoder) {
this.dataSource = dataSource;
this.bCryptPasswordEncoder = bCryptPasswordEncoder;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable().authorizeRequests()
.antMatchers(HttpMethod.GET, "/user/query").authenticated()
.antMatchers(HttpMethod.POST, "/user/register").permitAll()
.antMatchers(HttpMethod.POST, "/user/update").hasRole("USER")
.anyRequest().authenticated()
.and()
.addFilter(new JWTLoginFilter(authenticationManager()))
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.exceptionHandling().authenticationEntryPoint(new JWTAuthenticationEntryPoint());
}
/**
* 这里我采用的是jdbc的数据库表认证
**/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery(
"select username,password,true from t_user where username=?"
)
.authoritiesByUsernameQuery(
"select username,'ROLE_USER' from t_user where username=?"
)
.passwordEncoder(new BCryptPasswordEncoder());
}
}
实现过程中,通过继承UsernamePasswordAuthenticationFilter覆写了attemptAuthenticaton和successfulAuthentication方法,来看看源码中的该类
(截取重点部分)
只接接受 /login路径下的post请求 原则是post请求不可更改,而路径可以随意设置 如上述代码中的 super.setFilterProcessesUrl("/auth");
这里可以看到 其实在源码实现层面上,内部使用的便是 UsernamePasswordAuthenticationToken 这也是为什么我们在携带jwt再次请求的时候要解析jwt生成 APAT(英文简写吧),所以不需要调到attempt方法但仍然可以具备授权身份。
// 截留了源码中的关键语句
private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
SecurityContextHolder.getContext().setAuthentication(authResult);
successHandler.onAuthenticationSuccess(request, response, authResult);
}
验证成功的处理方法,处于AbstractAuthenticat… Filter中,有兴趣可以自己调出来看看,默认是调用Handler类处理后续,所以如果选择不覆写此方法的话可以自己去是写一个实现Hadnler类也能达到想要的自定义登录成功/失败处理。
再来看看JWTAuthentication的父类 BsicAuthenticationFitler
点开后 - - 有很大一篇的备注,看看重点
我们只需要关注两个关键点
1、我们携带token的请求被谁拦截处理(如上图)所以我们可以扩展它
2、怎么处理这个token 并完成识别用户权限。
SecurityContextHolder.getContext().setAuthentication(authResult);
这个语句便是设置认证信息的关键所在,再来会看一下这个频繁提到的UsernamePasswordAuthenticationToken到底是怎样的数据结构
该类有个主体字段 principal
输出一下验证账号密码后生成的UPAT看看
org.springframework.security.core.userdetails.User@a0ccb02b:
Username: 771007760;
Password: [PROTECTED];
Enabled: true;
AccountNonExpired: true;
credentialsNonExpired: true;
AccountNonLocked: true;
Granted Authorities: ROLE_USER
几乎囊括了用户的的所有信息。该类的一个构造器(上述在JWTAuthenticationFilter调用的构造器)
我们携带的JwtToken解析生成回 UPAT,只需写入username,null,role 便可完成认证。
输出一下通过token解析反生成的UPAT看看
System.out.println(u.getPrincipal()+" "+u.getAuthorities());
771007760 [ROLE_USER]
没有其他的信息,因为此时这就具备了鉴权条件了 username+role
参考了多方文档与实现方案 也没办法标出全部的借鉴,所以在此感谢所有大佬吧。
源码地址:https://github.com/YoungerJam/SpringSecurityWithJwtDemo