SpringSecurity-14-SpringSecurity结合JWT实现前后端分离的后端授权
什么是JWT
JWT是JSON WEB TOKEN的缩写,它是基于RFC 7519标准定义的一种可以安全传输的JSON对象,因为使用了数字签名,所以可以信任。
JWT的组成
JWT token的格式:header.payload.signature
-
header中用于存放签名的生成算法
{"alg": "HS512"}
-
payload用于存放用户名、token的生成时间和过期时间
{"sub":"admin","created":1489079981393,"exp":1489684781}
-
signature为以header和payload生成的签名,一旦header和payload被篡改,验证将失败
//secret为加密算法的密钥
String signature = HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
JWT实例
这是一个JWT的字符串
eyJhbGciOiJIUzUxMiJ9.eyJleHAiOjE2NDg5ODg1MjAsInN1YiI6ImFkbWluIiwiY3JlYXRlZCI6MTY0ODk4NDkyMDQyNX0.P8YJ5AhcKATEpUmdtSmzGXcdDacESZ2jqU20JpjCqZOqy5AEE2uelYtay--Kg2wRWFx3bBhf9A5Jbv2S8fbs_A
可以在该网站上获得解析结果:https://jwt.io/
编码实现
环境准备工作
- 建立Spring Boot项目并集成了Spring Security,项目可以正常启动
- 通过controller写一个HTTP的GET方法服务接口,比如:“/student/selectall”
- 实现最基本的动态数据验证及权限分配,即实现UserDetailsService接口。这两个接口都是向Spring Security提供用户、角色、权限等校验信息的接口
- 如果你学习过Spring Security的formLogin登录模式,请将HttpSecurity配置中的formLogin()配置段全部去掉。因为JWT完全使用JSON接口,没有from表单提交。
- HttpSecurity配置中一定要加上csrf().disable(),即暂时关掉跨站攻击CSRF的防御。这样是不安全的,我们后续章节再做处理。
以上实现可以去查看我的专题SpringBoot和SpringSecurity进行查看。
在pom.xml中添加项目依赖
org.springframework.boot
spring-boot-starter-security
io.jsonwebtoken
jjwt
0.9.0
org.springframework.boot
spring-boot-starter-web
mysql
mysql-connector-java
runtime
com.baomidou
mybatis-plus-boot-starter
3.5.1
cn.hutool
hutool-all
5.5.7
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
org.springframework.security
spring-security-test
test
在application.yml中加入如下自定义一些关于JWT的配置
jwt:
header: JWTName
secret: springkhbd
expiration: 360
-
jwt.header
的value是Http的header中存储JWT的名称,名字可读性越差越安全 -
jwt.secret
用来对JWT基础信息进行加密和解密的密匙。 -
jwt.expiration
用来设置JWT令牌的有效时间
添加JWT token的工具类JwtTokenUtil
JwtTokenUtil用于生成和解析JWT token的工具类
主要方法:
- generateToken(UserDetails userDetails):根据用户信息生成token令牌
- getUserNameFromToken(String token):根据token令牌获取用户名
- validateToken(String token, UserDetails userDetails):判断用户是否过期
- refreshToken(String token):根据token属性token的过期时间
package com.security.learn.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
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
@Slf4j
@ConfigurationProperties(prefix = "jwt")
@Component
public class JwtTokenUtil {
private static final String CLAIM_KEY_USERNAME = "sub";
private static final String CLAIM_KEY_CREATED = "created";
private String secret;
private Long expiration;
private String header;
/**
* 生成token令牌
*
* @param userDetails 用户
* @return 令token牌 */
public String generateToken(UserDetails userDetails) {
Map claims = new HashMap<>(2);
claims.put(CLAIM_KEY_CREATED, userDetails.getUsername());
claims.put(CLAIM_KEY_CREATED, new Date());
//生成Token
return generateToken(claims);
}
/**
* 从claims生成令牌
* @param claims
* @return */
private String generateToken(Map claims) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/**
* 从Token中获取用户名称
* @param token
* @return */
public String getUserNameFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 从令牌中获取数据声明,如果看不懂就看谁调用它
*
* @param token 令牌
* @return 数据声明 */
private Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
} catch (Exception e) {
log.info("JWT格式验证失败:{}",token);
claims = null;
}
return claims;
}
/**
* 生成token的过期时间 */
private Date generateExpirationDate() {
return new Date(System.currentTimeMillis() + expiration * 1000);
}
/**
* 根据token过去过期时间
* @param token
* @return */
private Date getExpiredDateFromToken(String token) {
Claims claims = getClaimsFromToken(token);
return claims.getExpiration();
}
/**
*
* 验证Token是否过期
* @param token
* @param userDetails
* @return true表示没有过期,false表示过期 */
public boolean validateToken(String token, UserDetails userDetails) {
String username = getUserNameFromToken(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}
/**
* 判断令牌是否过期
* @param token
* @return */
public Boolean isTokenExpired(String token) {
try {
Claims claims = getClaimsFromToken(token);
Date expiration = claims.getExpiration();
return expiration.before(new Date());
} catch (Exception e) {
return false;
}
}
/**
* 判断token是否可以刷新
* @param token
* @return */
public boolean canRefresh(String token) {
return !isTokenExpired(token);
}
/**
* 刷新token */
public String refreshToken(String token) {
Claims claims = getClaimsFromToken(token);
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
}
UserDetailsService接口的实现
@Component("myUserDetailsService")
@Slf4j
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Autowired
private AuthoritiesMapper authoritiesMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("认证请求: "+ username);
QueryWrapper wrapper = new QueryWrapper<>();
wrapper.eq("username",username);
List userEntities = userMapper.selectList(wrapper);
if (userEntities.size()>0){
QueryWrapper wrapper1 = new QueryWrapper<>();
wrapper.eq("userId", userEntities.get(0).getId());
List authorities = authoritiesMapper.selectList(wrapper1);
return new User(username, userEntities.get(0).getPassword(), AuthorityUtils.createAuthorityList(authorities.toString()));
}
return null;
}
}
开发登录接口(获取Token的接口)
JwtAdminService接口
public interface JwtAdminService {
/**
* 登录功能
* @param username 用户名
* @param password 密码
* @return 生成的JWT的token */
String login(String username, String password);
/**
* 刷新Token
* @param oldToken
* @return */
String refreshToken(String oldToken);
}
JwtAdminService接口实现
@AllArgsConstructor
@Slf4j
@Service
public class JwtAdminServiceImpl implements JwtAdminService {
private final UserDetailsService customUserDetailsService;
private final JwtTokenUtil jwtTokenUtill;
private final PasswordEncoder passwordEncoder;
/**
* 根据用户名密码登录时生成Token
* @param username 用户名
* @param password 密码
* @return */
@Override
public String login(String username, String password) {
try{
//根据用户名获取 用户信息
UserDetails userDetails = customUserDetailsService.loadUserByUsername(username);
if(!passwordEncoder.matches(password,userDetails.getPassword())){
throw new BadCredentialsException("密码不正确");
}
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
SecurityContextHolder.getContext().setAuthentication(token);
}catch (AuthenticationException e){
log.error("用户名或者密码不正确");
}
//生成JWT
UserDetails userDetails = customUserDetailsService.loadUserByUsername( username );
return jwtTokenUtill.generateToken(userDetails);
}
@Override
public String refreshToken(String oldToken) {
if (!jwtTokenUtill.isTokenExpired(oldToken)) {
return jwtTokenUtill.refreshToken(oldToken);
}
return null;
}
}
JwtAuthController的实现
- "/login"接口用于登录验证,并且生成JWT返回给客户端
- "/refreshtoken"接口用于刷新JWT,更新JWT令牌的有效期
@RestController
public class JwtAuthController {
@Resource
private JwtAdminService jwtAuthService;
@PostMapping(value = "/login")
public Result login(@RequestBody Map map) throws Exception {
String username = map.get("username");
String password = map.get("password");
if (StrUtil.isEmpty(username) || (StrUtil.isEmpty(password))) {
return Result.fail("用户名密码不能为空");
}
try{
return Result.data( jwtAuthService.login(username, password));
}catch(Exception e){
return Result.fail(e.getMessage());
}
}
@PostMapping(value = "/refreshtoken")
public String refresh(@RequestHeader("${jwt.header}") String token) {
return jwtAuthService.refreshToken(token);
}
}
添加SpringSecurity的配置类LearnSrpingSecurity
import com.security.learn.filter.JwtAuthenticationTokenFilter;
import com.security.learn.handler.RestAuthenticationEntryPoint;
import com.security.learn.handler.RestfulAccessDeniedHandler;
import org.springframework.beans.factory.annotation.Autowired;
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.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* 安全配置类 */
@EnableWebSecurity
public class LearnSrpingSecurity extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService myUserDetailsService;
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
private RestfulAccessDeniedHandler restfulAccessDeniedHandler;
@Autowired
private RestAuthenticationEntryPoint restAuthenticationEntryPoint;
/**
* 认证管理器
* 1.认证信息提供方式(用户名、密码、当前用户的资源权限)
* 2.可采用内存存储方式,也可能采用数据库方式等
* @param auth
* @throws Exception */
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService);
}
/**
* 资源权限配置(过滤器链):
* 1、被拦截的资源
* 2、资源所对应的角色权限
* 3、定义认证方式:httpBasic 、httpForm
* 4、定制登录页面、登录请求地址、错误处理方式
* 5、自定义 spring security 过滤器
* @param http
* @throws Exception */
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable() //禁用跨站csrf攻击防御,后面的章节会专门讲解
.sessionManagement()// 基于token,所以不需要session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/login").permitAll()//不需要通过登录验证就可以被访问的资源路径
.anyRequest().authenticated();
//添加自定义未授权和未登录结果返回
http.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)
.authenticationEntryPoint(restAuthenticationEntryPoint);
}
}
相关依赖以及方法说明
-
configure(HttpSecurity http)
:资源权限配置(过滤器链)、jwt过滤器及出异常后的处理器; -
configure(AuthenticationManagerBuilder auth)
:用于配置UserDetailsService
及PasswordEncoder
; -
RestfulAccessDeniedHandler
:当用户没有访问权限时的处理器,用于返回JSON格式的处理结果; -
RestAuthenticationEntryPoin
t:当未登录或token失效时,返回JSON格式的结果; -
UserDetailsService
:SpringSecurity定义的核心接口,用于根据用户名获取用户信息,需要自行实现; -
JwtAuthenticationTokenFilter
:在用户名和密码校验前添加的过滤器,如果有jwt的token,会自行根据token信息进行登录。 - configure(HttpSecurity http),主要配置:
- 将我们的自定义jwtAuthenticationTokenFilter,加载到UsernamePasswordAuthenticationFilter的前面。
- 因为我们使用了JWT,表明了我们的应用是一个前后端分离的应用,所以我们可以开启STATELESS禁止使用session
添加RestfulAccessDeniedHandler
当访问接口没有权限时,自定义的返回结果
/**
* 当访问接口没有权限时,自定义的返回结果 */
@Component
public class RestfulAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.getWriter().println(JSONUtil.parse(Result.fail(e.getMessage())));
response.getWriter().flush();
}
}
添加RestAuthenticationEntryPoint
当用户未登录或者token失效访问接口时,自定义的返回结果
/**
* 当未登录或者token失效访问接口时,自定义的返回结果
*/
@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.getWriter().println(JSONUtil.parse(Result.fail(authException.getMessage())));
response.getWriter().flush();
}
}
添加JwtAuthenticationTokenFilter
在用户名和密码校验前添加的过滤器,如果请求中有jwt的token且有效,会取出token中的用户名,然后调用SpringSecurity的API进行登录操作。
@Slf4j
@Component
@AllArgsConstructor
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private final UserDetailsService myUserDetailsService;
private final JwtTokenUtil jwtTokenUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String jwt = request.getHeader(jwtTokenUtil.getHeader());
if(!StrUtil.isEmpty(jwt)){
//根据jwt获取用户名
String username = jwtTokenUtil.getUserNameFromToken(jwt);
log.info("校验username:{}",username);
//如果可以正确从JWT中提取用户信息,并且该用户未被授权
if(!StrUtil.isEmpty(username) && SecurityContextHolder.getContext().getAuthentication()==null){
UserDetails userDetails = this.myUserDetailsService.loadUserByUsername(username);
if(jwtTokenUtil.validateToken(jwt,userDetails)){
//给使用该JWT令牌的用户进行授权
UsernamePasswordAuthenticationToken authenticationToken
= new UsernamePasswordAuthenticationToken(userDetails,null,
userDetails.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
}
filterChain.doFilter(request, response);
}
}
测试
测试登录接口,即:获取token的接口。输入正确的用户名、密码即可获取token
- 使用不带token,但是不传递JWT令牌,结果是禁止访问
- 使用不带token,携带JWT令牌
如果您觉得本文不错,欢迎关注,点赞,收藏支持,您的关注是我坚持的动力!
原创不易,转载请注明出处,感谢支持!如果本文对您有用,欢迎转发分享!
关注公众号 springboot葵花宝典 我将持续更新,并且获取我搜集的spingboot资料,谢谢!