1.添加token依赖
在pom.xml中添加以下依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
2.密钥配置文件
在resources目录下新建jwt.properties文件用来配置密钥
secretKey=aABfwdeUympsYdY4h6tHzpZNzvXa6zmpVdQzi2hEtwqXDVAZAgvcb5FaR4gAYfpHepYKyYMy
#这里的值是加密密钥,可以去网上搜密码加密器生成
3.token工具类
新建一个JwtUntil的java类,建议放在untils目录下,用来生成、校验token
public class JwtUntil {
// 使用SLF4J日志框架创建一个Logger实例,用于记录日志信息
private static final Logger logger = LoggerFactory.getLogger(JwtUntil.class);
// 从配置文件中加载的密钥,用于JWT的签名和验证
private static final String secretKey = loadSecretKey();
// 设置JWT的过期时间为604800秒(即1周)
// private static final Long expiration = 1209600L;
private static final Long expiration = 604800L;
/**
* 加载JWT使用的密钥。
* <p>
* 该方法从类路径下的配置文件`jwt.properties`中加载密钥。如果配置文件不存在或密钥未被正确指定,
* 则抛出运行时异常,以确保应用程序在启动时能够正确配置密钥。
*
* @return 返回配置文件中指定的密钥字符串。
* @throws RuntimeException 如果配置文件加载失败或密钥未被正确指定,抛出运行时异常。
*/
private static String loadSecretKey() {
// 创建Properties对象,用于存储配置文件中的键值对
Properties properties = new Properties();
try {
// 使用JwtUtil类的类加载器加载配置文件jwt.properties
// 这个方法会返回一个输入流,用于读取配置文件内容
//这里的jwt.properties是密钥
InputStream inputStream = JwtUntil.class.getClassLoader().getResourceAsStream("jwt.properties");
// 检查输入流是否为null,如果是,则表示配置文件未找到
if (inputStream == null) {
// 抛出FileNotFoundException,表示配置文件未找到
throw new FileNotFoundException("配置文件jwt.properties未找到");
}
// 使用Properties对象加载输入流中的配置信息
properties.load(inputStream);
// 从Properties对象中获取名为"secretKey"的属性值
// 这个值是从配置文件中读取的密钥
String secretKey = properties.getProperty("secretKey");
// 检查获取到的密钥是否为空或null
if (secretKey == null || secretKey.isEmpty()) {
// 如果密钥未被正确指定,抛出IllegalArgumentException
throw new IllegalArgumentException("配置文件中未指定secretKey");
}
// 如果一切正常,返回配置文件中指定的密钥
return secretKey;
} catch (FileNotFoundException e) {
// 如果配置文件未找到,记录错误日志,并抛出运行时异常
logger.error("配置文件jwt.properties未找到", e);
throw new RuntimeException("配置文件加载失败,无法启动应用", e);
} catch (IllegalArgumentException e) {
// 如果配置文件中未指定密钥,记录错误日志,并抛出运行时异常
logger.error("配置文件中未指定secretKey", e);
throw new RuntimeException("配置文件加载失败,无法启动应用", e);
} catch (Exception e) {
// 捕获其他所有异常,记录错误日志,并抛出运行时异常
// 这包括了Properties加载过程中可能发生的任何异常
logger.error("密钥验证失败", e);
throw new RuntimeException("密钥加载失败,无法启动应用", e);
}
}
/**
* 生成用户token,并设置token的超时时间。
* 这个方法接受用户名作为参数,并创建一个包含用户名信息的JWT。
* JWT的过期时间被设置为从生成时起的1周。
*
* @param userNumber 账号
* @return 生成的JWT字符串
*/
public static String createToken(String userNumber) {
// 检查用户名是否为空,避免创建无效的token
if (userNumber == null || userNumber.isEmpty()) {
throw new IllegalArgumentException("账号不能为空");
}
// 设置JWT的过期时间为当前时间加上1周(1周的毫秒数)
long twoWeeksInMilliseconds = 7 * 24 * 60 * 60 * 1000L;
//创建了一个Date对象,表示从当前时间起加上一周后的日期和时间,这个日期和时间将被用作JWT的过期时间
Date expireDate = new Date(System.currentTimeMillis() + twoWeeksInMilliseconds);
// 创建一个Map来存储JWT头部信息
Map<String, Object> headerMap = new HashMap<>();
headerMap.put("alg", "HS256"); // 指定签名算法为HMAC256
headerMap.put("typ", "JWT"); // 指定令牌类型为JWT
// 使用Auth0的JWT库创建JWT
// 首先创建一个JWT.builder()来配置JWT的各个部分
JWTCreator.Builder builder = JWT.create()
.withHeader(headerMap) // 添加头部信息
.withClaim("userNumber", userNumber) // 添加账号
.withExpiresAt(expireDate) // 设置过期时间
.withIssuedAt(new Date()) // 设置签发时间
.withIssuer("sdApp"); // 可选:设置发行者
//这里的 "yourIssuer" 是一个占位符,你应该替换为实际的发行者名称,例如你的应用或服务的名称
// 检查secretKey是否已加载,避免签名时出现空指针异常
if (secretKey == null || secretKey.isEmpty()) {
throw new IllegalStateException("密钥未加载或为空");
}
// 使用密钥进行签名,并构建最终的JWT字符串
String token = builder.sign(Algorithm.HMAC256(secretKey));
// 记录生成的JWT,便于调试和监控
logger.info("Token created for user {}: {}", userNumber, token);
return token;
}
/**
* 校验token并解析token。
* 这个方法接受一个JWT字符串作为参数,并验证其有效性。
* 如果token有效,返回true;如果token无效或过期,返回false。
*
* @param token 需要验证的JWT字符串
* @return true如果token有效,false如果token无效或过期
*/
public static boolean verifyToken(String token) {
// 检查传入的token是否为空,如果为空直接返回false
if (token == null || token.isEmpty()) {
logger.error("令牌空值");
return false;
}
try {
// 使用Auth0的JWT库构建一个JWTVerifier实例,用于验证JWT
// 这里假设secretKey是一个已经定义好的密钥,用于HMAC256算法
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(secretKey)).build();
// 使用验证器验证JWT,如果token无效或被篡改,会抛出JWTVerificationException异常
DecodedJWT jwt = verifier.verify(token);
// 检查JWT是否过期,如果过期则返回false
if (jwt.getExpiresAt().before(new Date())) {
// 如果JWT过期,记录警告日志并返回false
logger.warn("令牌已过期");
return false;
}
// 如果JWT有效且未过期,返回true
return true;
} catch (JWTVerificationException e) {
// 如果JWT验证失败,记录错误日志并返回false
// 这可能发生在token无效、被篡改或签名不匹配时
logger.error("令牌验证错误: {}", e.getMessage());
return false;
} catch (Exception e) {
// 如果发生其他异常,记录错误日志并返回false
// 这可能是由于内部错误或配置问题导致的
logger.error("令牌验证过程中出现意外错误", e);
return false;
}
}
4.拦截器
新建JwtInterceptor的Java类,建议放到interceptor目录下
JWT(JSON Web Tokens)拦截器是一种在Web应用程序中用于验证JWT的服务器端组件。它的作用是在请求到达具体的Controller之前检查HTTP请求头中是否包含有效的JWT,并且该JWT是否给予了访问特定资源的权限。以下是JWT拦截器的主要功能和实现细节:
主要功能
- 验证JWT:检查每个请求是否包含一个有效的JWT,并验证该令牌的有效性。
- 权限控制:确保只有带有有效JWT的请求才能访问受保护的资源。
- 日志记录:记录JWT验证过程中的重要信息,便于调试和监控。
- 错误处理:当JWT无效或过期时,拦截器会返回适当的HTTP状态码和错误信息。
/**
* JwtInterceptor类实现了HandlerInterceptor接口,用于拦截进入Controller之前的请求,
* 并验证请求头中的JWT是否有效。
*/
@Component
public class JwtInterceptor implements HandlerInterceptor {
/**
* 使用LoggerFactory创建一个Logger实例,用于记录日志信息。
*/
private static final Logger logger = LoggerFactory.getLogger(JwtInterceptor.class);
/**
* preHandle方法在请求处理之前被调用,用于在Controller方法执行之前进行预处理。
* 如果该方法返回true,则请求继续向下传递到下一个拦截器或Controller;
* 如果返回false,则请求将被中断,不会继续处理。
*
* @param request HttpServletRequest对象,表示当前请求。该对象包含请求的信息,如请求头、查询参数等。
* @param response HttpServletResponse对象,表示当前响应。该对象用于构造响应,如设置状态码、响应头等。
* @param handler 当前请求的处理者,可能是Controller方法或另一个拦截器。这个参数可以用来判断请求是否被其他拦截器处理过。
* @return boolean值,表示是否继续执行请求。如果返回true,则请求继续向下传递;如果返回false,则请求将被中断。
* @throws Exception 可能抛出的异常,例如在处理请求或响应时发生的异常。
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
/**
* 从请求头中获取"Authorization"字段,它应该包含Bearer令牌。
* 这个令牌用于验证用户的身份。
*/
String token = request.getHeader("Authorization");
/**
* 检查token是否为空或不以"Bearer "开头。
* 如果令牌丢失或无效,设置响应状态为401 Unauthorized,并返回错误信息。
*/
if (token == null || !token.startsWith("Bearer ")) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("令牌丢失或无效");
return false; // 中断请求
}
/**
* 提取令牌字符串,去掉"Bearer "前缀。
* 这个字符串是JWT的实际内容,需要被验证。
*/
String authToken = token.substring(7);
/**
* 使用JwtUtil验证令牌是否有效。
* JwtUtil类提供了JWT的生成和验证功能。
*/
boolean isValid = JwtUntil.verifyToken(authToken);
if (!isValid) {
/**
* 如果令牌无效或已过期,设置响应状态为401 Unauthorized,并返回错误信息。
*/
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("令牌无效或已过期");
return false; // 中断请求
}
/**
* 如果令牌验证成功,返回true以继续执行下一个拦截器或Controller。
*/
return true;
}
}
5.注册拦截器配置文件
JwtInterceptorConfig
/**
* JwtInterceptor类实现了HandlerInterceptor接口,用于拦截进入Controller之前的请求,
* 并验证请求头中的JWT是否有效。
*/
@Component
public class JwtInterceptor implements HandlerInterceptor {
/**
* 使用LoggerFactory创建一个Logger实例,用于记录日志信息。
*/
private static final Logger logger = LoggerFactory.getLogger(JwtInterceptor.class);
/**
* preHandle方法在请求处理之前被调用,用于在Controller方法执行之前进行预处理。
* 如果该方法返回true,则请求继续向下传递到下一个拦截器或Controller;
* 如果返回false,则请求将被中断,不会继续处理。
*
* @param request HttpServletRequest对象,表示当前请求。该对象包含请求的信息,如请求头、查询参数等。
* @param response HttpServletResponse对象,表示当前响应。该对象用于构造响应,如设置状态码、响应头等。
* @param handler 当前请求的处理者,可能是Controller方法或另一个拦截器。这个参数可以用来判断请求是否被其他拦截器处理过。
* @return boolean值,表示是否继续执行请求。如果返回true,则请求继续向下传递;如果返回false,则请求将被中断。
* @throws Exception 可能抛出的异常,例如在处理请求或响应时发生的异常。
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
/**
* 从请求头中获取"Authorization"字段,它应该包含Bearer令牌。
* 这个令牌用于验证用户的身份。
*/
String token = request.getHeader("Authorization");
/**
* 检查token是否为空或不以"Bearer "开头。
* 如果令牌丢失或无效,设置响应状态为401 Unauthorized,并返回错误信息。
*/
if (token == null || !token.startsWith("Bearer ")) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("令牌丢失或无效");
return false; // 中断请求
}
/**
* 提取令牌字符串,去掉"Bearer "前缀。
* 这个字符串是JWT的实际内容,需要被验证。
*/
String authToken = token.substring(7);
/**
* 使用JwtUtil验证令牌是否有效。
* JwtUtil类提供了JWT的生成和验证功能。
*/
boolean isValid = JwtUntil.verifyToken(authToken);
if (!isValid) {
/**
* 如果令牌无效或已过期,设置响应状态为401 Unauthorized,并返回错误信息。
*/
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("令牌无效或已过期");
return false; // 中断请求
}
/**
* 如果令牌验证成功,返回true以继续执行下一个拦截器或Controller。
*/
return true;
}
}
6.用户认证配置文件
SecurityConfig
// 使用@Configuration注解声明这是一个配置类,这样Spring容器就会自动扫描并加载这个类
@Configuration
// 使用@EnableWebSecurity注解启用Spring Security的Web安全功能
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 覆盖WebSecurityConfigurerAdapter的configure方法,用于配置HttpSecurity对象
@Override
protected void configure(HttpSecurity http) throws Exception {
// 禁用CSRF保护,因为在REST API中通常不需要CSRF保护
http.csrf().disable()
// 配置请求授权规则
.authorizeRequests()
// 对于/login和/register路径下的请求,允许所有用户访问
.antMatchers("/login", "/register").permitAll()
// 其他所有请求都需要用户认证
.anyRequest().authenticated()
// 配置认证方式为HTTP基本认证
.and()
.httpBasic();
}
// 覆盖WebSecurityConfigurerAdapter的configure方法,用于配置AuthenticationManagerBuilder对象
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 设置用户详情服务和密码编码器
auth.userDetailsService(userDetailsService())
.passwordEncoder(passwordEncoder());
}
// 定义一个Bean,返回一个BCryptPasswordEncoder实例,用于密码加密
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
7.自定义Mapper方法
/**
UserMapper
* UserMapper接口,继承自BaseMapper,提供对User实体的基本数据库操作。
*/
@Mapper // 1. 标记这个接口为MyBatis的Mapper接口,MyBatis会为这个接口生成代理实现类
public interface UserMapper extends BaseMapper<User> { // 2. 继承自BaseMapper,User表示这个Mapper操作的实体类
// 这里可以添加UserMapper接口特有的方法,例如自定义的数据库查询方法
// MyBatis会自动为这个接口生成一个实现类,并在内部使用XML或注解配置的SQL语句
// 添加用户
@Insert("INSERT INTO user(userNumber, password, username) VALUES(#{userNumber}, #{password}, #{username})")
void registerUser(
@Param("userNumber") String userNumber,
@Param("password") String password,
@Param("username") String username);
//自定义查询账号方法
@Select("SELECT userNumber, password FROM user WHERE userNumber = #{userNumber}")
User selectLogin(@Param("userNumber") String userNumber);
}
8.Service业务登录方法
在此之前,,我们得先创建IUserService接口继承IService<User>
然后创建UserServiceImpi类去实现重写方法
/**
* UserServiceImpl类,继承自ServiceImpl<UserMapper, User>并实现了IUserService接口,
* 提供了用户服务的具体实现。
*/
@Service // 1. 标记这个类为Spring的一个Service,使其成为Spring上下文中的一个Bean
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Autowired
UserMapper userMapper;
public String login(User user) {
User newUser = userMapper.selectLogin(user.getUserNumber());
if (newUser != null && newUser.getPassword().equals(user.getPassword())){
// 用户名和密码匹配,生成JWT
return JwtUntil.createToken(user.getUserNumber());
}
// 验证失败
return "用户名或密码错误";
}
}
9.创建LoginController实现登录验证
@RestController
@RequestMapping("/login")
public class LoginController {
@Autowired
private UserServiceImpl userService;
@PostMapping
public String login(@RequestBody User user) {
System.out.println(user);
return userService.login(user);
}
}
注册测试
账号密码均为123456(演示)
如图所示注册成功,我们用此账号试一试登录验证
登录验证测试
可以看到登录已经成功了,返回了一段带有请求头数据的加密token
用户名或密码错误演示