SpringBoot整合SpringSecurity实现用户登录验证及JWT令牌生成
登录Demo完整代码仓库地址:https://gitee.com/strivezhangp/java-study-log.git(分支为logindemo)
1 整体概述
本篇介绍了SpringBoot
整合以及SpringSecurity
实现简单的用户登录效果。未连接数据库,在 .yml
文件中定义了用户名和密码, 包含以下功能:
- 使用
SpringBoot
初始化项目SpringBoot2.4.0, JDK版本选用1.8
- 使用
Maven
包管理工具 - 配置整合
SpringSecurity
- 模拟用户登录
- 生成
JWT
存入请求头header
2 实现步骤
2.1 初始化项目
创建一个SpringBoot
项目,载入web
支持,然后在Maven配置文件中导入一些基本依赖。见下:
依赖及用途介绍:
spring-boot-starter-security
SpringSecurity注入jjwt
JWT生成工具
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- springboot security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- jwt 版本问题 报错后 需要额外引入的工具类-->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
2.2 SpringSecurity验证流程
以下流程图根据 原作者:https://gitee.com/markerhub/VueAdmin.git 讲解后绘制,只做个人观点。
2.3 封装一个可以被序列化的Result类
序列化的Result实现消息的返回。这里涉及到为什么要实现序列化的消息返回,个人见解如下:
远程通信:在Java的RMI(远程方法调用)或通过网络发送对象(如使用HTTP协议发送JSON或XML对象)时,对象需要被序列化,以便可以在网络上传输。在接收端,对象会被反序列化以恢复其原始状态。
/**
* 返回消息封装类
*/
@Data
public class Result implements Serializable {
private int code;
private String msg;
private Object data;
private static Result success(int code, String msg, Object data) {
Result result = new Result();
result.setCode(code);
result.setMsg(msg);
result.setData(data);
return result;
}
private static Result success(Object data) {
return success(200, "操作成功", data);
}
private static Result failure(int code, String msg, Object data) {
Result result = new Result();
result.setCode(code);
result.setMsg(msg);
result.setData(data);
return result;
}
private static Result failure(String msg) {
return failure(400, "操作失败", null);
}
}
2.4 基本的Security
配置
创建一个Security
配置类,实现Spring-Security
的全局拦截配置,具体参照 2.2 的验证流程。主要配置以下几个部分:
- 配置跨域和预防攻击相关选项
- 配置登录处理器 成功、失败处理器
- 配置
session
生成规则等 - 配置拦截过滤的白名单(一些不需要安全认证拦截【用户登录后才能访问】的请求:项目
logo
、login
、logout
、验证码获取等)
注意:Security默认登录的请求为 /login
登出请求 /logout
项目整合Security后会自动路由到一个登录页面,实现登陆验证。再没有连接数据库实现用户认证的时候,在 .yml
配置文件中设置用户名等。
spring:
# spring security 配置
security:
user:
password: 111
name: 111
/** 部分代码 */
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 白名单
private static final String[] URL_WHITELIST = {存放不需要登陆后才能访问的请求地址}
/** 自动装配必要类*/
// 全局配置
protected void configure(HttpSecurity http) throws Exception {
http.cors().and()
.csrf().disable() // 跨域配置 关闭预防攻击
.formLogin() // 登录配置
.successHandler(userLoginSuccessHandler) // 登录成功处理器
.failureHandler(userLoginFailureHandler) // 登录失败处理器
.and()
.logout().logoutSuccessHandler(userLogOutSuccessHandler) // 登出处理器
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // session配置 生成规则
.and()
// 配置白名单 除白名单外,其余链接需要登录后访问
.authorizeRequests().antMatchers(URL_WHITELIST).permitAll().anyRequest().authenticated();
}
}
2.5 自定义登录成功处理器-生成JWT令牌
在用户登陆成功后,生成JWT
并将其添加到后续的用户请求头部分,确保当前用户访问其他接口时,在请求头部分存有用户认证信息。
实现JWT
的生成需要借助JWT
生成工具-- jjwt
依赖,构建JWT生成类,在此类中注解部分添加了 .yml
配置文件中需要配置的JWT
属性的前缀,同时定义了一些必要的属性,构建当前用户的JWT,还实现了7天过期的功能。具体类的实现如下:
@Data
@Component
@ConfigurationProperties(prefix = "login.jwt")
public class JwtUtil {
private long expire; // 过期时长 单位 ms
private String secret; // JWT密钥
private String header;
/**
* JWT生成
* @param username
* @return
*/
public String generateToken(String username){
Date nowDate = new Date();
Date expireDate = new Date(nowDate.getTime() + 1000 * expire); // 计算过期时间
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setSubject(username)
.setIssuedAt(nowDate)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512, secret) // 加密算法
.compact();
}
}
login:
jwt:
header: Authorization
expire: 604800 #过期时间 7 天(单位:秒)
secret: ji8n3439n439n43ld9ne9343fdfer49h # 32位的密钥设置
登录成功处理器的定义如下:
@Component
public class UserLoginSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
JwtUtil jwtUtil;
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=UTF-8");
ServletOutputStream outputStream = httpServletResponse.getOutputStream();
// 生成JWT 以用户名为JWT签发主题
String jwt = jwtUtil.generateToken(authentication.getName());
// 生成的JWT放到请求头中
httpServletResponse.setHeader(jwtUtil.getHeader(), jwt);
System.out.println(jwt);
Result result = Result.success("登录成功");
outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
}
注意:由于版本问题可能使用JWT
生成工具的时候会导致报错–找不到类等,此外需要2.1初始化项目时 xml
文件中额外引入的一些依赖。
测试:PostMan
测试后,登录成功后请求头中存在了一个Authorization
字段,存放生成的JWT
令牌。
2.6 自定义登陆失败处理器
根据异常类型,返回用户登录失败的原因信息,具体定义如下:
@Component
public class UserLoginFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=UTF-8");
ServletOutputStream outputStream = httpServletResponse.getOutputStream();
String errorMessage;
// 判断异常类型是否为一个异常的实例,进而返回不同的错误信息
if (e instanceof BadCredentialsException) {
errorMessage = "用户名或密码错误";
} else {
errorMessage = "其他失败原因";
}
Result result = Result.failure(errorMessage);
outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
}
2.7 自定义用户登出处理器
用户登出的操作,security
默认请求路径为 /logout
,登出的时候首先判断用户是否存在身份凭证信息,然后清除令牌信息。
@Component
public class UserLogOutSuccessHandler implements LogoutSuccessHandler {
@Autowired
JwtUtil jwtUtil;
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
// 判断是否有身份凭证
if (authentication != null) {
new SecurityContextLogoutHandler().logout(httpServletRequest, httpServletResponse, authentication);
}
httpServletResponse.setContentType("application/json;charset=UTF-8");
ServletOutputStream outputStream = httpServletResponse.getOutputStream();
httpServletResponse.setHeader(jwtUtil.getHeader(), "");
outputStream.write(JSONUtil.toJsonStr(Result.success("退出成功")).getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
}
附:自定义登录登出路径的配置
当然,如果没有自定义这些请求表单、访问的路径,security将会自动绑定的请求路径是 /login
, 登出路径为 /logout
在security配置类中定义如下:
http
// 配置请求授权
.authorizeRequests()
.antMatchers("/login**", "/custom-logout**").permitAll() // 允许所有人访问登录和登出路径
.anyRequest().authenticated() // 其他所有请求都需要认证
.and()
// 配置表单登录
.formLogin()
.loginPage("/custom-login") // 自定义登录页路径
.loginProcessingUrl("/perform-login") // 自定义登录处理路径
.permitAll()
.and()
// 配置登出
.logout()
.logoutUrl("/custom-logout") // 自定义登出请求路径
.logoutSuccessUrl("/custom-logout-success") // 自定义登出成功后的跳转路径
.permitAll();
附:未进行跨域配置登录请求中的一些参数说明
在Spring Security
中,_csrf
字段是跨站请求伪造(Cross-Site Request Forgery
,简称CSRF
)防护机制的一部分。CSRF
是一种攻击方式,攻击者通过伪造用户的请求来执行未经授权的操作。
Spring Security
使用同步令牌模式来防止CSRF攻击。每当一个表单被渲染到客户端时,Spring Security
会生成一个唯一的CSRF
令牌并将其附加到表单数据中。当用户提交表单时,浏览器会将这个令牌一起发送回服务器。服务器会验证这个令牌是否有效,并且是否与会话中存储的令牌相匹配。如果令牌无效或不匹配,请求将被拒绝,从而防止CSRF
攻击。
在未进行跨域的时候 login
请求携带一个参数:_csrf=0b229dba-1f13-48da-b47a-96ee254aa007
就是这样一个CSRF
令牌。它确保了表单提交请求是来自合法用户且未被篡改的。_csrf
字段用于在客户端和服务器之间传递CSRF
令牌,以提供对CSRF
攻击的保护。
消除这个字段的影响可以在Security
配置文件中设置如下:
http.csrf().disable();
后续将实现整合MybatisPlus的用户查库验证以及图片验证码功能。