spring security 6.3.3 + java-jwt 3.10.3
·
spring security 6.3.3 + java-jwt 3.10.3 模拟前后端分离使用token验证登录
例子中实现了用户通过security验证成功后获取后端返回的token,在token有效期内可以访问受保护的 /user_request 接口,通过token验证后获取后端返回的数据
项目结构

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.security</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.51</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.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-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter-test</artifactId>
<version>3.0.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>maven_central</id>
<name>Maven Central</name>
<url>https://repo.maven.apache.org/maven2/</url>
</repository>
</repositories>
</project>
pojo - DbUser
package com.security.demo.pojo;
import java.io.Serializable;
/**
* 和数据库里所有字段分别对应的实体类
*/
public class DbUser implements Serializable {
private int id;
private String username;
private String password;
private String source;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getSource() {
return source;
}
public void setSource(String source) {
this.source = source;
}
@Override
public String toString() {
return "DbUser{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", source='" + source + '\'' +
'}';
}
}
pojo - LoginUser
package com.security.demo.pojo;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
/**
* 实现了UserDetails接口的实体类,以供security用来做后续是否有权限登录的校验
*/
public class LoginUser implements Serializable, UserDetails {
/**
* 使用DbUser做为该实体类的字段
* 因为LoginUser的getUsername()和getPassword()两个方法返回的值
* 可以从DbUser里获得
*/
private DbUser dbUser;
public DbUser getUser() {
return dbUser;
}
public void setUser(DbUser dbUser) {
this.dbUser = dbUser;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of();
}
@Override
public String getPassword() {
return dbUser.getPassword();
}
@Override
public String getUsername() {
return dbUser.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return UserDetails.super.isAccountNonExpired();
}
@Override
public boolean isAccountNonLocked() {
return UserDetails.super.isAccountNonLocked();
}
@Override
public boolean isCredentialsNonExpired() {
return UserDetails.super.isCredentialsNonExpired();
}
@Override
public boolean isEnabled() {
return UserDetails.super.isEnabled();
}
@Override
public String toString() {
return "LoginUser{" + "DbUser=" + dbUser + "}";
}
}
pojo - ResultData
package com.security.demo.pojo;
import java.io.Serializable;
/**
* 封装返回前端的统一格式
*/
public class ResultData implements Serializable {
private int code;
private String msg;
private Object data;
private ResultData() {}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
public Object getData() {
return data;
}
/**
* 链式调用设置属性入参
*/
public static class ResultDataBuild {
private ResultDataBuild() {}
ResultData resultData = new ResultData();
public ResultDataBuild setCode(int code) {
resultData.code = code;
return this;
}
public ResultDataBuild setMsg(String msg) {
resultData.msg = msg;
return this;
}
public ResultDataBuild setData(Object object) {
resultData.data = object;
return this;
}
public ResultData build() {
return resultData;
}
}
public static ResultDataBuild builder() {
return new ResultDataBuild();
}
}
LoginUserAuthorization
package com.security.demo.config.authentication;
import com.security.demo.dao.DemoDao;
import com.security.demo.pojo.LoginUser;
import com.security.demo.pojo.DbUser;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import java.util.Objects;
/**
* 实现UserDetailsService接口的自定义UserDetailsService类型的类
* 在自定义的AuthenticationManager中会使用到该类
* 重写了loadUserByUsername(),里边获取到从DB里查出来相同username的信息
* 返回UserDetails类型的LoginUser,后续AuthenticationManager会自动进行是否有权限登录的校验
*/
@Component
public class LoginUserAuthorization implements UserDetailsService {
private static final Logger LOGGER = LoggerFactory.getLogger(LoginUserAuthorization.class);
@Resource
private DemoDao demoDao;
@Override
public UserDetails loadUserByUsername(String username) {
DbUser dbUser = null;
try {
dbUser = demoDao.queryByName(username);
} catch (Exception e) {
LOGGER.error("在这里准备记录一个可能不会发生的错误");
}
if (Objects.isNull(dbUser)) {
LOGGER.error("库里不存在该用户名字");
throw new UsernameNotFoundException("用户名或密码错误");
}
LoginUser loginUser = new LoginUser();
loginUser.setUser(dbUser);
return loginUser;
}
}
TokenAuthorization
package com.security.demo.config.authentication;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.security.demo.utils.RedisUtil;
import com.security.demo.utils.TokenUtil;
import jakarta.annotation.Resource;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.Objects;
/**
* OncePerRequestFilter是每个请求只经过一次该过滤器
*/
@Component
public class TokenAuthorization extends OncePerRequestFilter {
@Resource
private RedisUtil redisUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("token");
if (!StringUtils.hasText(token)) {
//放行让后边的过滤器判断
filterChain.doFilter(request, response);
return;
}
//解析token
String authorization = TokenUtil.parseToken(token);
Object value = redisUtil.getKey(authorization);
if (Objects.isNull(authorization) && Objects.isNull(value)) {
throw new RuntimeException("token无效");
}
//从redis里获取存入的信息
JSONObject object = JSON.parseObject(value.toString());
//封装信息放入SecurityContextHolder
UsernamePasswordAuthenticationToken authenticationToken;
//credentials和authorities目前没有都设为null
authenticationToken = new UsernamePasswordAuthenticationToken(object, null, null);
//验证信息放入SecurityContextHolder上下文
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//继续放行
filterChain.doFilter(request, response);
}
}
RedisConfig
package com.security.demo.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.StringRedisSerializer;
/**
* Redis配置类
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
//key使用string序列化
template.setKeySerializer(new StringRedisSerializer());
//value使用string序列化
template.setValueSerializer(new StringRedisSerializer());
//初始化
template.afterPropertiesSet();
return template;
}
}
SecurityConfig
package com.security.demo.config;
import com.security.demo.config.authentication.LoginUserAuthorization;
import com.security.demo.config.authentication.TokenAuthorization;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* security的配置类
*/
@Configuration
public class SecurityConfig {
@Resource
private LoginUserAuthorization loginUserAuthorization;
@Resource
private TokenAuthorization tokenAuthorization;
/**
* 定义了password使用的加密方式
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 为AuthenticationManager里定义了UserDetailsService当中方法的执行逻辑和password的加密方式
* ProviderManager是AuthenticationManager接口的具体实现类
* 该类里实现了authenticate方法是做具体验证的
* @return
*/
@Bean
public AuthenticationManager authenticationManager() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(loginUserAuthorization);
provider.setPasswordEncoder(passwordEncoder());
return new ProviderManager(provider);
}
/**
* security全是由过滤器链的方式实现的
* 该例中只做了一些最简单的配置
* @param http
* @return
* @throws Exception
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
//本地测试关了csrf
http.csrf(AbstractHttpConfigurer::disable);
//只允许/demo/register接口可以不受任何限制的访问
http.authorizeHttpRequests(r -> r.requestMatchers("/demo/user_login", "/demo/register").permitAll()
//对于其它任何接口的访问都要经过身份认证方可
.anyRequest().authenticated());
//使用security美观简洁的默认登录页,使用postman模拟前后端分离登录则不需要该登录页
//http.formLogin(Customizer.withDefaults());
//将自定义token验证过滤器放在UsernamePasswordAuthenticationFilter之前
http.addFilterBefore(tokenAuthorization, UsernamePasswordAuthenticationFilter.class);
//返回配置好的过滤器链逻辑
return http.build();
}
}
TokenUtil
package com.security.demo.utils;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Date;
/**
* 生成token和解析token的工具类
*/
public class TokenUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(TokenUtil.class);
//应该算是不常见的密钥
public static final String SECRET = "ABCDEFG_HIGKLMN_12345_67890";
//默认过期时间1个小时=3600000毫秒
public static final long EXPIRATION = 3600000;
//使用密钥生成该算法的实例
private static final Algorithm ALGORITHM = Algorithm.HMAC256(SECRET);
/**
* 生成Token
* @param id
* @return
*/
public static String generateToken(String id) {
String token = null;
try {
token = JWT.create()
//设置过期时间
.withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION))
//设置payload, 里边准备存放的是UUID
.withClaim("Authorization", id)
//设置签名
.sign(ALGORITHM);
} catch (Exception e) {
LOGGER.error("生成JWT出现异常");
return token;
}
return token;
}
/**
* 解析Token
* @param token
* @return
*/
public static String parseToken(String token) {
String authorization = null;
try {
//返回verifier实例
JWTVerifier verifier = JWT.require(ALGORITHM).build();
//使用verifier实例验证Token并获取Base64格式的payload内容
String payload = verifier.verify(token).getPayload();
//使用Base64解码
byte[] decode = Base64.getDecoder().decode(payload);
//将解码后的字节使用UTF-8转为可读String
String string = new String(decode, StandardCharsets.UTF_8);
//将String内容转为JsonObject并获取需要的数据
JSONObject jsonObject = JSON.parseObject(string);
authorization = jsonObject.get("Authorization").toString();
} catch (JWTVerificationException e) {
LOGGER.error("验证token出现异常");
return authorization;
}
return authorization;
}
}
RedisUtil
package com.security.demo.utils;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.Objects;
/**
* Redis存取值工具类
*/
@Component
public class RedisUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(RedisUtil.class);
@Resource
private RedisTemplate<String, Object> redisTemplate;
public boolean setKeyValue(String key, Object value) {
boolean result = false;
try {
redisTemplate.opsForValue().set(key, value);
result = true;
} catch (Exception e) {
LOGGER.error("Redis在存放时出现异常");
}
return result;
}
public Object getKey(String key) {
Object object = null;
if (Objects.isNull(key) || key.isBlank()) {
return object;
}
try {
object = redisTemplate.opsForValue().get(key);
} catch (Exception e) {
LOGGER.error("Redis取值出现异常");
}
return object;
}
public boolean deleteKey(String key) {
boolean result = false;
try {
result = redisTemplate.delete(key);
} catch (Exception e) {
LOGGER.error("Redis册除值出现异常");
}
return result;
}
}
DemoController
package com.security.demo.controller;
import com.alibaba.fastjson2.JSON;
import com.security.demo.pojo.DbUser;
import com.security.demo.pojo.ResultData;
import com.security.demo.service.DemoService;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/demo")
public class DemoController {
private static final Logger LOGGER = LoggerFactory.getLogger(DemoController.class);
@Resource
private DemoService demoService;
/**
* 获取所有已存在的用户
* @return
*/
@RequestMapping("/list")
public String list() {
List<DbUser> dbUsers = demoService.queryAll();
return JSON.toJSONString(dbUsers);
}
/**
* 注册一个新用户
* @param dbUser
* @return
*/
@RequestMapping("/register")
public String register(@RequestBody DbUser dbUser) {
String username = dbUser.getUsername();
int row = demoService.register(dbUser);
if (row == 0) {
username = username.concat(": 注册失败,用户名已存在");
LOGGER.error("{}:", username);
}
if (row == 1) {
username = username.concat(": 注册成功");
LOGGER.info("{}:", username);
}
return username;
}
/**
* 模拟用户登录通过验证并返回token
* @param dbUser
* @return
*/
@RequestMapping("/user_login")
public ResultData login(@RequestBody DbUser dbUser) {
return demoService.login(dbUser);
}
/**
* 携带着登录后返回的token在访问受保护的接口时解析token成功后返回数据
* @param token
* @return
*/
@RequestMapping("/user_request")
public ResultData requestData(@RequestHeader("token") String token) {
return demoService.requestData(token);
}
}
DemoService
package com.security.demo.service;
import com.security.demo.pojo.DbUser;
import com.security.demo.pojo.ResultData;
import java.util.List;
public interface DemoService {
List<DbUser> queryAll();
int register(DbUser dbUser);
ResultData login(DbUser dbUser);
ResultData requestData(String token);
}
DemoServiceImpl
package com.security.demo.service.impl;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.security.demo.dao.DemoDao;
import com.security.demo.pojo.DbUser;
import com.security.demo.pojo.ResultData;
import com.security.demo.service.DemoService;
import com.security.demo.utils.RedisUtil;
import com.security.demo.utils.TokenUtil;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
@Service
public class DemoServiceImpl implements DemoService {
private static final Logger LOGGER = LoggerFactory.getLogger(DemoServiceImpl.class);
@Resource
private DemoDao demoDao;
@Resource
private PasswordEncoder passwordEncoder;
@Resource
private AuthenticationManager authenticationManager;
@Resource
private RedisUtil redisUtil;
/**
* 获取所有用户信息
* @return
*/
@Override
public List<DbUser> queryAll() {
return demoDao.queryAll();
}
/**
* 注册一个新用户
* @param dbUser
* @return
*/
@Override
public int register(DbUser dbUser) {
int row = 0;
if (checkUserNameIsExist(dbUser.getUsername())) {
return row;
}
try {
String password = dbUser.getPassword();
String encode = passwordEncoder.encode(password);
dbUser.setPassword(encode);
row = demoDao.register(dbUser);
} catch (Exception e) {
LOGGER.error("用户在注册时出现异常");
}
return row;
}
@Override
public ResultData login(DbUser dbUser) {
try {
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(dbUser.getUsername(), dbUser.getPassword());
authenticationManager.authenticate(authenticationToken);
} catch (AuthenticationException e) {
return ResultData.builder().setCode(403).setMsg("用户名或密码错误").setData(null).build();
}
String uuid = UUID.randomUUID().toString();
String token = TokenUtil.generateToken(uuid);
redisUtil.setKeyValue(uuid, JSON.toJSONString(dbUser));
return ResultData.builder().setCode(200).setMsg("登录成功并获取token").setData(Map.of("token", token)).build();
}
@Override
public ResultData requestData(String token) {
String authorization = TokenUtil.parseToken(token);
Object object = redisUtil.getKey(authorization);
JSONObject jo = JSON.parseObject(object.toString());
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(jo.get("username"), jo.get("password"));
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
if (Objects.isNull(authenticate)) {
throw new RuntimeException("用户信息错误");
}
List<DbUser> dbUsers = demoDao.queryAll();
return ResultData.builder().setCode(200).setMsg("查到所有信息").setData(Map.of("userAll", dbUsers)).build();
}
/**
* 在注册的时候检查该用户名字是否已存在
* @param username
* @return
*/
private boolean checkUserNameIsExist(String username) {
boolean result = false;
try {
DbUser dbUser = demoDao.checkUserByName(username);
if (!Objects.isNull(dbUser)) {
result = true;
LOGGER.error("新用户名字已存在");
}
} catch (Exception e) {
LOGGER.error("在这里准备记录一个可能不会发生的错误");
}
return result;
}
}
DemoDao
package com.security.demo.dao;
import com.security.demo.pojo.DbUser;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface DemoDao {
//获取所有用户信息
List<DbUser> queryAll();
//根据username获取对应的用户信息
DbUser queryByName(String username);
//注册一个新用户
int register(DbUser dbUser);
//检查username是否已存在
DbUser checkUserByName(String username);
}
更多推荐


所有评论(0)