SpringBoot集成SpringSecurity⽤JWT令牌实现登录和鉴权的⽅法
最近在做项⽬的过程中需要⽤JWT做登录和鉴权查了很多资料都不甚详细
有的是需要在l⾥进⾏jwt的配置但我在导包后并没有相应的配置项因⽽并不适⽤
在踩过很多坑之后稍微整理了⼀下做个笔记
⼀、概念
1、什么是JWT
Json Web Token (JWT)是为了在⽹络应⽤环境间传递声明⽽执⾏的⼀种基于JSON的开放标准(RFC 7519)
该token被设计为紧凑且安全的特别适⽤于分布式站点的单点登录(SSO)场景
随着JWT的出现使得校验⽅式更加简单便捷化
JWT实际上就是⼀个字符串它由三部分组成:头部载荷和签名
⽤[.]分隔这三个部分最终的格式类似于:
在服务器直接根据token取出保存的⽤户信息即可对token的可⽤性进⾏校验使得单点登录更为简单
2、JWT校验的过程
1、浏览器发送⽤户名和密码发起登录请求
2、服务端验证⾝份根据算法将⽤户标识符打包⽣成token字符串并且返回给浏览器
3、当浏览器需要发起请求时将token⼀起发送给服务器
4、服务器发现数据中携带有token 随即进⾏解密和鉴权
5、校验成功服务器返回请求的数据
⼆、使⽤
1、⾸先是导包
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Spring Security和JWT整合 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.10.RELEASE</version>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- 字符串转换需要⽤到此包 -->
<dependency>
<groupId>org.apachemons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.4</version>
</dependency>
2、实体类
两个实体类⼀个是⽤户另⼀个是权限
public class User {
private Integer id;
private String username;
private String password;
省略gettersetter之类的代码...
}
public class Role {
private Integer id;
private String username;
private String name;
省略gettersetter之类的代码...
}
3、然后需要⼀个Utils⼯具类
该类⽤于进⾏Token的加密和解密可在此类中单元测试
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JwtTokenUtil {
/
/ Token请求头
public static final String TOKEN_HEADER = "Authorization";
// Token前缀
public static final String TOKEN_PREFIX = "Bearer ";
// 签名主题
public static final String SUBJECT = "piconjo";
// 过期时间
public static final long EXPIRITION = 1000 * 24 * 60 * 60 * 7;
// 应⽤密钥
public static final String APPSECRET_KEY = "piconjo_secret";
// ⾓⾊权限声明
private static final String ROLE_CLAIMS = "role";
/**
* ⽣成Token
*/
public static String createToken(String username,String role) {
Map<String,Object> map = new HashMap<>();
map.put(ROLE_CLAIMS, role);
String token = Jwts
.builder()
.setSubject(username)
.setClaims(map)
.
claim("username",username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRITION))
.signWith(SignatureAlgorithm.HS256, APPSECRET_KEY)pact();
return token;
}
/**
* 校验Token
*/
public static Claims checkJWT(String token) {
try {
final Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
return claims;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 从Token中获取username
*/
public static String getUsername(String token){
Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
("username").toString();
}
/**
* 从Token中获取⽤户⾓⾊
*/
public static String getUserRole(String token){
Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
("role").toString();
}
/**
* 校验Token是否过期
*/
public static boolean isExpiration(String token){
Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
Expiration().before(new Date());
}
}
4、配置UserDetailsService的实现类⽤于加载⽤户信息
x.bean.Role; // ⾃⼰的包
x.bean.User; // ⾃⼰的包
x.mapper.UserMapper; // ⾃⼰的包
import org.springframework.beans.factory.annotation.Autowired;
import org.authority.SimpleGrantedAuthority;
import org.userdetails.UserDetails;
import org.userdetails.UserDetailsService;
import org.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
if (s == null || "".equals(s))
{
throw new RuntimeException("⽤户不能为空");
}
// 调⽤⽅法查询⽤户
User user = userMapper.findUserByUsername(s);
if (user == null)
{
throw new RuntimeException("⽤户不存在");
}
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (Role role:userMapper.findRoleByUsername(s))
{
authorities.add(new SimpleGrantedAuthority("ROLE_"+Name()));
}
return new org.userdetails.Username(),"{noop}"+Password(),authorities);  }
}
5、然后配置两个
其中⼀个⽤于登录另⼀个⽤于鉴权
JWTAuthenticationFilter登录:
该⽤于获取⽤户登录的信息
⾄于具体的验证只需创建⼀个token并调⽤authenticationManager的authenticate()⽅法
让Spring security验证即可验证的事交给框架
import com.alibaba.fastjson.JSON;
x.utils.JwtTokenUtil; // ⾃⼰的包
import org.springframework.security.authentication.*;
import org.Authentication;
import org.AuthenticationException;
import org.GrantedAuthority;
import org.userdetails.User;
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.Collection;
/**
* 验证⽤户名密码正确后⽣成⼀个token并将token返回给客户端
*/
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
public JWTAuthenticationFilter(AuthenticationManager authenticationManager)
{
this.authenticationManager = authenticationManager;
}
/**
* 验证操作接收并解析⽤户凭证
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request,HttpServletResponse response) throws AuthenticationException {
// 从输⼊流中获取到登录的信息
// 创建⼀个token并调⽤authenticationManager.authenticate() 让Spring security进⾏验证
springboot和过滤器return authenticationManager.authenticate(new Parameter("username"),Parameter("password")));
}
/**
* 验证【成功】后调⽤的⽅法
* 若验证成功⽣成token并返回
*/
@Override
protected void successfulAuthentication(HttpServletRequest request,HttpServletResponse response,FilterChain chain,Authentication authResult) throws IOException {
User user= (User) Principal();
// 从User中获取权限信息
Collection<? extends GrantedAuthority> authorities = Authorities();
// 创建Token
String token = Username(), String());
// 设置编码防⽌乱码问题
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
// 在请求头⾥返回创建成功的token
// 设置请求头为带有"Bearer "前缀的token字符串
response.setHeader("token", JwtTokenUtil.TOKEN_PREFIX + token);
// 处理编码⽅式防⽌中⽂乱码
response.setContentType("text/json;charset=utf-8");
// 将反馈塞到HttpServletResponse中返回给前台
}
/**
* 验证【失败】调⽤的⽅法
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {    String returnData="";
// 账号过期
if (failed instanceof AccountExpiredException) {
returnData="账号过期";
}
// 密码错误
else if (failed instanceof BadCredentialsException) {
returnData="密码错误";
}
// 密码过期
else if (failed instanceof CredentialsExpiredException) {
returnData="密码过期";
}
/
/ 账号不可⽤
else if (failed instanceof DisabledException) {
returnData="账号不可⽤";
}
//账号锁定
else if (failed instanceof LockedException) {
returnData="账号锁定";
}
// ⽤户不存在
else if (failed instanceof InternalAuthenticationServiceException) {
returnData="⽤户不存在";
}
// 其他错误
else{
returnData="未知异常";
}
// 处理编码⽅式防⽌中⽂乱码
response.setContentType("text/json;charset=utf-8");
// 将反馈塞到HttpServletResponse中返回给前台
}
}
JWTAuthorizationFilter权限校验:
当访问需要权限校验的URL(当然该URL也是需要经过配置的) 则会来到此在该中对传来的Token进⾏校验
只需告诉Spring security该⽤户是否已登录并且是什么⾓⾊拥有什么权限即可
x.utils.JwtTokenUtil; // ⾃⼰的包
import org.apachemons.lang3.StringUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.authority.SimpleGrantedAuthority;
import org.ontext.SecurityContextHolder;
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.ArrayList;
import java.util.Collection;
/**
* 登录成功后⾛此类进⾏鉴权操作
*/
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {
public JWTAuthorizationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
/**
* 在过滤之前和之后执⾏的事件
*/
@Override
protected void doFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain chain) throws IOException, ServletException {
String tokenHeader = Header(JwtTokenUtil.TOKEN_HEADER);
// 若请求头中没有Authorization信息或是Authorization不以Bearer开头则直接放⾏
if (tokenHeader == null || !tokenHeader.startsWith(JwtTokenUtil.TOKEN_PREFIX))
{
chain.doFilter(request, response);
return;
}
// 若请求头中有token 则调⽤下⾯的⽅法进⾏解析并设置认证信息
super.doFilterInternal(request, response, chain);
}
/**
* 从token中获取⽤户信息并新建⼀个token
*
* @param tokenHeader 字符串形式的Token请求头
* @return 带⽤户名和密码以及权限的Authentication
*/
private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) {
// 去掉前缀获取Token字符串
String token = place(JwtTokenUtil.TOKEN_PREFIX, "");
// 从Token中解密获取⽤户名
String username = Username(token);
// 从Token中解密获取⽤户⾓⾊
String role = UserRole(token);
// 将[ROLE_XXX,ROLE_YYY]格式的⾓⾊字符串转换为数组
String[] roles = StringUtils.strip(role, "[]").split(", ");
Collection<SimpleGrantedAuthority> authorities=new ArrayList<>();
for (String s:roles)
{
authorities.add(new SimpleGrantedAuthority(s));
}
if (username != null)
{
return new UsernamePasswordAuthenticationToken(username, null,authorities);
}
return null;
}
}
6、再配置⼀个⾃定义类⽤于进⾏匿名⽤户访问资源时⽆权限的处理
该类需实现AuthenticationEntryPoint
import com.alibaba.fastjson.JSONObject;
import org.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 JWTAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.setCharacterEncoding("utf-8");
response.setContentType("text/javascript;charset=utf-8");
}
}
7、最后将这些组件组装到⼀起即可
创建⼀个⾃定义的配置类继承WebSecurityConfigurerAdapter
在该类上需加@EnableWebSecurity注解配置Web安全过滤器和启⽤全局认证机制
x.JWTAuthenticationEntryPoint; // ⾃⼰的包
x.xxx.JWTAuthenticationFilter; // ⾃⼰的包
x.xxx.JWTAuthorizationFilter; // ⾃⼰的包
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import t.annotation.Bean;
import org.fig.annotation.authentication.builders.AuthenticationManagerBuilder; import org.fig.annotation.web.builders.HttpSecurity;
import org.fig.figuration.EnableWebSecurity;
import org.fig.figuration.WebSecurityConfigurerAdapter;
import org.fig.http.SessionCreationPolicy;
import org.userdetails.UserDetailsService;
import org.s.CorsConfiguration;
import org.s.CorsConfigurationSource;
import org.s.UrlBasedCorsConfigurationSource;
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
@Qualifier("userDetailsServiceImpl")
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
/**
* 安全配置
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// 跨域共享
.and()
/
/ 跨域伪造请求限制⽆效
.csrf().disable()
.authorizeRequests()
// 访问/data需要ADMIN⾓⾊
.antMatchers("/data").hasRole("ADMIN")
// 其余资源任何⼈都可访问
.anyRequest().permitAll()
.and()
// 添加JWT登录
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
// 添加JWT鉴权
.
addFilter(new JWTAuthorizationFilter(authenticationManager()))
.sessionManagement()
// 设置Session的创建策略为:Spring Security永不创建HttpSession 不使⽤HttpSession来获取SecurityContext        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// 异常处理
.exceptionHandling()
// 匿名⽤户访问⽆权限资源时的异常
.authenticationEntryPoint(new JWTAuthenticationEntryPoint());
}
/**
* 跨域配置
* @return 基于URL的跨域配置信息
*/
@Bean
CorsConfigurationSource corsConfigurationSource() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
// 注册跨域配置
return source;
}
}
定义⼀个⽤于测试的对外映射接⼝:
@RestController
public class UserController {
@GetMapping("/data")
private ResponseUtil data()
{
return "This is data.";
}
}
默认登录路径是/login⽤POST请求发送
若要修改默认的登录路径只需要在⾃⼰定义的登录过滤器JWTAuthenticationFilter的构造⽅法⾥进⾏配置即可⽐如若想修改为/api/login:
public JWTAuthenticationFilter(AuthenticationManager authenticationManager)
{
this.authenticationManager = authenticationManager;
// 设置登录URL
super.setFilterProcessesUrl("/api/login");
}
登录时参数的属性名分别是username和password 不能改动:
登录成功后会返回⼀个Token:
在请求需要权限的接⼝路径时若不带上Token 则会提⽰没有访问权限
带上Token后再次请求即可正常访问:
注:Token的前⾯要带有Bearer的前缀