根据需要引入必要的依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.12.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
建表
这里大家自由发挥,只要有用户名和密码即可。
INSERT INTO study.tb_user VALUES (1, 'Lebron', '$2a$10$IOBCJNs4S3GeQKh/lARzBO.ed6up6GrEufxB.KYzG2VMxd.oaoYPm', '山羊', 1);
INSERT INTO study.tb_user VALUES (2, 'Stephen', '$2a$10$D5A6eBhX3RZLHjH5uxxOO.baVrrZ88MKzrquO19iNk4qTozCEb/yi', '库日天', 1);
INSERT INTO study.tb_user VALUES (3, 'Kevin', '$2a$10$BcfPaEioGF2i1FbuPo2DCOZ33KcqXG9OyVLvkgU5HoMqahWc9os9K', '包工头', 1);
其中密码使用BCryptPasswordEncoder加密,并手动插入数据库。
@Test
void testGenerate(){
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String Jpassword = "James";
String Cpassword = "Curry";
String Dpassword = "Durant";
String encodedJPassword = encoder.encode(Jpassword);
String encodedCPassword = encoder.encode(Cpassword);
String encodedDPassword = encoder.encode(Dpassword);
System.out.println("James: " + encodedJPassword);//$2a$10$IOBCJNs4S3GeQKh/lARzBO.ed6up6GrEufxB.KYzG2VMxd.oaoYPm
System.out.println("Curry: " + encodedCPassword);//$2a$10$D5A6eBhX3RZLHjH5uxxOO.baVrrZ88MKzrquO19iNk4qTozCEb/yi
System.out.println("Durant: " + encodedDPassword);//$2a$10$BcfPaEioGF2i1FbuPo2DCOZ33KcqXG9OyVLvkgU5HoMqahWc9os9K
}
SpringSecurity配置
package com.example.mzhusecurity.config;
import com.example.mzhusecurity.filter.JwtRequestFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
public class SecurityConfiguration {
@Autowired
private JwtRequestFilter jwtRequestFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 禁用csrf
http.csrf(AbstractHttpConfigurer::disable);
// 禁用session会话
http.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
// 所有请求都需要认证,认证方式:httpBasic
http.authorizeHttpRequests((auth) -> {
auth.requestMatchers("/auth/login","/auth/refresh","/register").permitAll() // 登录接口放行, 不然会被拦截
.anyRequest().authenticated();
}).httpBasic(Customizer.withDefaults());
// 在 UsernamePasswordAuthenticationFilter 之前添加 JwtRequestFilter
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
redis配置
package com.example.mzhusecurity.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
}
JWT工具类和JwtRequestFilter
JWT工具类主要用来生成和验证token,登出后的token失效会被放入黑名单中,黑名单用redis存放
package com.example.mzhusecurity.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwsHeader;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SecureDigestAlgorithm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.time.Instant;
import java.util.Date;
import java.util.UUID;
@Component
public class JwtUtil {
private static final int ACCESS_EXPIRE = 600; // 10 minutes
private static final int REFRESH_EXPIRE = 3600; // 1 hour
private final static SecureDigestAlgorithm<SecretKey, SecretKey> ALGORITHM = Jwts.SIG.HS256;
/**
* at least 32 bits, randomly generated, with no regular pattern
*/
private final static String SECRET = "K7GVa4n1dJ+3eMZ4VJGds0sFI/gQ9K5J3k3PZ3eW5iU=";
public static final SecretKey KEY = Keys.hmacShaKeyFor(SECRET.getBytes());
public final static String JWT_ISS = "mzhu";
public final static String SUBJECT = "noSubject";
/**
* store tokens added to black list because of logging out
*/
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public String generateAccessToken(String username) {
return generateJWT(username, ACCESS_EXPIRE);
}
public String generateRefreshToken(String username) {
return generateJWT(username, REFRESH_EXPIRE);
}
public static String generateJWT(String username, int expireTime) {
String uuid = UUID.randomUUID().toString();
Date exprireDate = Date.from(Instant.now().plusSeconds(expireTime));
return Jwts.builder()
.header()
.add("typ", "JWT")
.add("alg", "HS256")
.and()
.claim("username", username)
.id(uuid)
.expiration(exprireDate)
.issuedAt(new Date())
.subject(SUBJECT)
.issuer(JWT_ISS)
.signWith(KEY, ALGORITHM)
.compact();
}
public static Jws<Claims> parseClaim(String token) {
return Jwts.parser()
.verifyWith(KEY)
.build()
.parseSignedClaims(token);
}
public static JwsHeader parseHeader(String token) {
return parseClaim(token).getHeader();
}
public static Claims parsePayload(String token) {
return parseClaim(token).getPayload();
}
public static boolean validateToken(String token) {
try {
parseClaim(token);
return true;
} catch (Exception e) {
return false;
}
}
public boolean addToBlackList(String token) {
redisTemplate.opsForValue().set(token, "blacklist");
return true;
}
public boolean isBlackList(String token) {
//这里为了方便只判断了是否为空因为当前redis村的只有blacklist token,后续如果redis存了其他还要再判断是不是"blacklist"
return redisTemplate.opsForValue().get(token) != null;
}
}
JwtRequestFilter作为过滤链中的一环,从请求头中提取jwt进行解析并验证访问时间是否有效或者是否存在于黑名单。
package com.example.mzhusecurity.filter;
import com.example.mzhusecurity.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.ArrayList;
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
@Autowired
private JwtUtil jwtUtil;
@Override
protected void doFilterInternal(jakarta.servlet.http.HttpServletRequest request, jakarta.servlet.http.HttpServletResponse response, jakarta.servlet.FilterChain filterChain) throws jakarta.servlet.ServletException, IOException {
final String requestTokenHeader = request.getHeader("Authorization");
String username = null;
String jwtToken = null;
boolean blackflag = false;
// JWT Token is in the form "Bearer token". Remove Bearer word and get only the Token
if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
jwtToken = requestTokenHeader.substring(7);
// check if the token is in black list, i.e. it has been added to the black list when logging out
if (jwtUtil.isBlackList(jwtToken)) {
System.out.println("The token has expired. JWT Token is in black list");
blackflag = true;
}
// Parse the JWT Token
try {
username = jwtUtil.parsePayload(jwtToken).get("username", String.class);
} catch (Exception e) {
System.out.println("Unable to get JWT Token or JWT Token has expired");
}
} else {
logger.warn("JWT Token does not begin with Bearer String");
}
// Once we get the token validate it.
if (!blackflag && username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
if (jwtUtil.validateToken(jwtToken)) {
// If the token is valid configure Spring Security to manually set authentication
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
username, null, new ArrayList<>());
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
filterChain.doFilter(request, response);
}
}
验证过程
package com.example.mzhusecurity.security;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class UserAuthenticationProvider implements AuthenticationProvider {
@Autowired
private UserDetailsService userService;
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 从Authentication 对象中获取用户名和密码
String username = authentication.getName();
String password = authentication.getCredentials().toString();
System.out.println(username + "---" + password);
//根据用户名从数据库中加载出来对应user,这里不做过多演示
UserDetails user = userService.loadUserByUsername(username);
//随机的盐值不一样,加密后的密码也会有不同,但是同一个hash值所以能成功匹配
System.out.println("db password: " + user.getPassword());
System.out.println("encoded password: " + this.passwordEncoder().encode(password));
if (this.passwordEncoder().matches(password, user.getPassword())) {
System.out.println("Access Success: " + user);
return new UsernamePasswordAuthenticationToken(username, password, user.getAuthorities());
} else {
System.out.println("Access Denied: The username or password is wrong!");
throw new BadCredentialsException("The username or password is wrong!");
}
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
测试接口
验证接口
package com.example.mzhusecurity.controller;
import com.example.mzhusecurity.common.CommonResult;
import com.example.mzhusecurity.controller.vo.RefreshRequestVO;
import com.example.mzhusecurity.controller.vo.TokenResponseVO;
import com.example.mzhusecurity.security.UserAuthenticationProvider;
import com.example.mzhusecurity.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.bind.annotation.*;
import com.example.mzhusecurity.controller.vo.AuthRequestVO;
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private UserAuthenticationProvider userAuthenticationProvider;
@Autowired
private JwtUtil jwtUtil;
@PostMapping("/login")
public CommonResult<TokenResponseVO> createToken(@RequestBody AuthRequestVO authRequestVO) throws Exception {
try {
System.out.println(authRequestVO.getUsername() + ": " + authRequestVO.getPassword());
Authentication authentication = userAuthenticationProvider.authenticate(
new UsernamePasswordAuthenticationToken(authRequestVO.getUsername(), authRequestVO.getPassword()));
System.out.println(authentication);
} catch (AuthenticationException e) {
throw new Exception("Invalid username or password");
}
String accessToken = jwtUtil.generateAccessToken(authRequestVO.getUsername());
String refreshToken = jwtUtil.generateRefreshToken(authRequestVO.getUsername());
TokenResponseVO tokenResponse = new TokenResponseVO(accessToken, refreshToken);
return CommonResult.success(tokenResponse, "Login successful");
}
@PostMapping("/refresh")
public CommonResult<TokenResponseVO> refreshToken(@RequestBody RefreshRequestVO refreshRequestVO) throws Exception {
String refreshToken = refreshRequestVO.getRefreshToken();
if (jwtUtil.validateToken(refreshToken)) {
String username = jwtUtil.parsePayload(refreshToken).get("username", String.class);
String newAccessToken = jwtUtil.generateAccessToken(username);
TokenResponseVO tokenResponse = new TokenResponseVO(newAccessToken, refreshToken);
return CommonResult.success(tokenResponse, "Token refreshed successfully");
} else {
return CommonResult.failed("Invalid refresh token");
}
}
@PostMapping("/logout")
public CommonResult<Void> logout(@RequestHeader("Authorization") String token) {
if (token != null && token.startsWith("Bearer ")) {
String jwtToken = token.substring(7);
jwtUtil.addToBlackList(jwtToken);
return CommonResult.success(null, "Logout successful");
}
return CommonResult.failed("Invalid token");
}
}
页面接口测试
@RestController
public class PageController {
@GetMapping("/somePage")
public String index(){
return "Welcome to the page, you are authorized to access";
}
}
最终效果
登录请求设置参数username和password
通过上图accessToken访问PageController中的接口,可以看到访问成功
如果过了有效时间(设置的是十分钟),但没到refresh token过期的时间(设置的是一小时),可以通过refresh token请求新的access token
logout 后token就失效了