SpringBoot整合SpringSecurity + JWT
前置知识:Cookie,Session,Token
Cookie,Session介绍
Cookie 、 Session 和 Token 是用于在 Web 应用程序中管理用户状态和身份验证的技术。因为在 Web 应用中, HTTP的通信是无状态的,每个请求都是完全独立的,所以服务端无法确认当前访问者的身份信息,无法分辨上一次的请求发送者和这一次的发送者是不是同一个人。
Cookie 是由服务器发送给厍户浏览器的小型文本文件,存储在客户端的浏览器中。它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。服务器可以读取 Cookie 并使厍其中的信息来进行识别和个性化处理。
每个 cookie 都会绑定单一的域名,无法在别的域名下获取使用,一级域名和二级域名之间是允许共享使用的。cookie 是不可跨域的,并且每个域名下面的 cookie 的数量也是有限的。
Session 是在服务器端创建和管理的一种会活机制。当用户首次访问网站时,服务器会为该用户创建一个唯一的SessionID,通常通过 Cookie 在客户端进行存储。会话标识符在后续的清求中厍于标识具体是哪个厍户。通常情况下,session 是基于 cookie 实现的, session 存储在服务器端, sessionld 会帔存储到客户端的 cookie 中。
有了 cookie 和 session 之后,基本可以实现用户的各种验证、鉴权及身份识别了。但是还是有一些场景中,不是特别适合这两种方案,或者说这两种方案的话还不够,那么就需要token上场了。
主要由以下几个场景:
1 、跨域请求: Cookie 是不支持跨域的,当在不同的域名之间进行通信时,使用 Token 可以更方便地在跨域清求中传递身份验证信息,而不 Cookie 限制。
2 、分布式场景: Session 是存储在服务器上的,但是现在很多都是集群部署,这就使得 Session 也需要实现分布式 Session, 而如果能用 Token 的话,就可以不用这么复杂。
3 、 **API交互:**当我们使用浏览器访问后端服务的时候,可以用 cookie 和 session, 但是如果是 API 调用,比如Dubbo 交互,就没办法做 cookie 的存储和传递了,而使用 To ken 是常见的身份验证方式。客户端通过提供Token来证明其身份,并获得对受保护资源的访问权限。
4 、跨平台应用程序: Token 可以轻松地在不同的平台和设备之间共享和传递,而无需依赖特定的会话机制或Cookie 支持。
5 、**前后端分离顶目:**现在很多项目都是前后端分离的了,这种项目中,前端和后端之间涌过 API 的方式交互,这种的话用 Token 也会更加方便一些。
Token介绍
Token 也是一种用于用户身份鉴权的手段。他其实是一种代表用户身份验证和授权的令牌。在 Web 应用程序中,常用的身份验证方案是基于令牌的身份验证 (Token-based Authentication) 。当用户成功登录时,服务器会生成一个 Token 并将其返回给客户端。客户端在后续的请求中将 Token 包含在请求头或请求参数中发送给服务器。服务器接收到 Token 后,会进行验证和解析,以确定用户的身份和权限。Token 通常是基于某种加密算法生成的,因此具有一定的安全性。
session与token对比
- token 是无状态的,后端不需要记录信息,每次请求过来进行解密就能得到对应信息。
- session 是有状态的,需要后端每次去检索id的有效性。不同的session都需要进行保存,但也可以设置单点登录,减少保存的数据。
- session与token的选择是空间与时间博弈,为什么这么说呢,是因为token不需要保存,不占存储空间,但每次访问都需要进行解密,消耗了一定的时间。在一般的前后端分离项目中,token展现出了它的优势,成为了比session更好的选择。
JWT
JWT其实就是一种被广泛使用的token,它的全称是JSON Web Token,它通过数字签名的方式,以JSON对象为载体,在不同的服务终端之间安全地传输信息。
JWT最常见的使用场景就是授权认证,一旦用户登录,后续每个请求都将包含JWT,系统在每次处理用户请求之前,都要先进行JWT安全校验,通过之后再进行处理。
JWT由3部分组成,用.拼接,如:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
这三部分分别是:
**Header:**Header中保存了令牌类型type和所使用的的加密算法,例如:
{
'typ': 'JWT',
'alg': 'HS256'
}
**Payload:**Payload中包含的是请求体和其它一些数据,例如包含了和用户相关的一些信息
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
Signature:Signature签名属于 JWT 的第三部分。主要是把Header的base64UrlEncode与Payload的base64UrlEncode拼接起来,再进行HMACSHA256加密等最终得到的结果作为签名部分。例如:
// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
SpringSecurity
SpringSecurity 是 Spring 家族中的一个安全管理框架。相比与另外一个安全框架Shiro,它提供了更丰富的功能,社区资源也比Shiro丰富。
一般来说中大型的项目都是使用SpringSecurity 来做安全框架。小项目有Shiro的比较多,因为相比与SpringSecurity,Shiro的上手更加的简单。
一般Web应用的需要进行认证和授权。
认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户
授权:经过认证后判断当前用户是否有权限进行某个操作
而认证和授权也是SpringSecurity作为安全框架的核心功能。
SpringBoot整合SpringSecurity及JWT
1、添加pom依赖
<!-- 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>${jwt.version}</version>
</dependency>
2、添加JWT工具类
用于生成解析token
application.yml
# token配置
token:
# 令牌自定义标识
header: Authorization
# 令牌密钥
secret: fhgaf%^$#%cHDFDhUHLKnhkhj
# 令牌有效期(默认30分钟)
expireTime: 2628000
JwtUtils
@Data
@Component
@ConfigurationProperties(prefix = "token")
public class JwtUtils {
private long expireTime;
private String secret;
private String header;
protected static final long MILLIS_SECOND = 1000;
protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;
private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L;
// 生成JWT
public String generateToken(String account) {
Date nowDate = new Date();
Date expireDate = new Date(nowDate.getTime() + MILLIS_SECOND * expireTime);
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setSubject(account)
.setIssuedAt(nowDate)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
// 解析JWT
public Claims getClaimsByToken(String jwt) {
try {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(jwt)
.getBody();
} catch (Exception e) {
return null;
}
}
// 判断JWT是否过期
public boolean isTokenExpired(Claims claims) {
return claims.getExpiration().before(new Date());
}
}
3、添加ResponseResult类统一接口返回结果
import java.util.HashMap;
/**
* @Classname ResponseResult
* @Description: 请求响应模板
* @Author: zengxi
* @CreateDate: 2023/11/28 19:32
*/
public class ResponseResult extends HashMap<String, Object> {
private static final long serialVersionUID = 1L;
private String MESSAGE_TAG = "message";
private String CODE_TAG = "code";
private String DATA_TAG = "data";
/**
* 初始化一个新创建的 ResponseResult 对象,使其表示一个空消息。
*/
public ResponseResult() {
}
/**
* 初始化一个新创建的 ResponseResult 对象
*
* @param code 状态码
* @param message 返回内容
*/
public ResponseResult(int code, String message) {
super.put(CODE_TAG, code);
super.put(MESSAGE_TAG, message);
}
/**
* 初始化一个新创建的 ResponseResult 对象
*
* @param code 状态码
* @param message 返回内容
* @param data 返回数据
*/
public ResponseResult(int code, String message, Object data) {
super.put(CODE_TAG, code);
super.put(MESSAGE_TAG, message);
if (data != null) {
super.put(DATA_TAG, data);
}
}
/**
* 返回成功消息
*/
public static ResponseResult success() {
return new ResponseResult(200, "操作成功", null);
}
/**
* 返回成功消息
*
* @param data 返回数据
*/
public static ResponseResult success(Object data) {
return new ResponseResult(200, "操作成功", data);
}
/**
* 返回成功消息
*
* @param message 返回内容
*/
public static ResponseResult success(String message) {
return new ResponseResult(200, message, null);
}
/**
* 返回成功消息
*
* @param message 返回内容
* @param data 返回数据
*/
public static ResponseResult success(String message, Object data) {
return new ResponseResult(200, message, data);
}
/**
* 返回失败消息
*/
public static ResponseResult error() {
return new ResponseResult(500, "操作失败", null);
}
/**
* 返回失败消息
*
* @param message 返回内容
*/
public static ResponseResult error(String message) {
return new ResponseResult(500, message, null);
}
/**
* 返回失败消息
*
* @param data 返回内容
*/
public static ResponseResult error(Object data){
return new ResponseResult(500, "操作失败", data);
}
/**
* 返回失败消息
*
* @param message 返回内容
* @param data 返回数据
*/
public static ResponseResult error(String message, Object data) {
return new ResponseResult(500, message, data);
}
/**
* 方便链式调用
*
* @param key 键
* @param value 值
* @return 数据对象
*/
@Override
public ResponseResult put(String key, Object value)
{
super.put(key, value);
return this;
}
}
3、实现UserDetailsService接口,重写其中的方法
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.easynote.common.domain.LoginUser;
import com.example.easynote.module.user.domain.User;
import com.example.easynote.module.user.mapper.UserMapper;
import lombok.extern.slf4j.Slf4j;
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.Service;
import javax.annotation.Resource;
import java.util.Objects;
/**
* @Classname UserDetailsServiceImpl
* @Description: 实现springSecurity UserDetailsService接口
* @Author: zengxi
* @CreateDate: 2024/1/11 20:30
*/
@Service
@Slf4j
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String account) throws UsernameNotFoundException {
// 根据用户名查询用户信息
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getAccount,account);
User user = userMapper.selectOne(wrapper);
// 如果查询不到数据就通过抛出异常来给出提示
if(Objects.isNull(user)){
log.info("登录用户:{} 不存在",account);
throw new RuntimeException("登录用户:" + account + "不存在");
}
// 封装成UserDetails对象返回
return new LoginUser(user);
}
}
我们可以看到重写的方法loadUserByUsername返回的结果是SpringSecurity中的UserDetails接口
所以我们要将自己的User对象转换为UserDetails,故我们写了一个LoginUser类实现了UserDetails
import com.example.easynote.module.user.domain.User;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
/**
* @Classname LoginUser
* @Description: 实现UserDetails
* @Author: zengxi
* @CreateDate: 2024/1/11 20:35
*/
@Data
public class LoginUser implements UserDetails {
private static final long serialVersionUID = 1L;
private User user;
public LoginUser(){
}
public LoginUser(User user){
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getAccount();
}
/**
* 账户是否未过期,过期无法验证
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 指定用户是否解锁,锁定的用户无法进行身份验证
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 指示是否已过期的用户的凭据(密码),过期的凭据防止认证
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 是否可用 ,禁用的用户不能身份验证
*/
@Override
public boolean isEnabled()
{
return true;
}
}
4、实现登录逻辑
import com.example.easynote.common.domain.LoginUser;
import com.example.easynote.common.domain.ResponseResult;
import com.example.easynote.common.utils.JwtUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Objects;
/**
* @Classname LoginServcie
* @Description: 登录逻辑实现
* @Author: zengxi
* @CreateDate: 2024/1/11 21:34
*/
@Service
public class LoginServcie {
@Resource
private AuthenticationManager authenticationManager;
@Resource
private JwtUtils jwtUtils;
public String login(String account, String password) {
UsernamePasswordAuthenticationToken
authenticationToken = new UsernamePasswordAuthenticationToken(account, password);
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
if (Objects.isNull(authenticate)) {
throw new RuntimeException("用户名或密码错误");
}
// 使用account生成token
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
String userId = loginUser.getUser().getUserId().toString();
return jwtUtils.generateToken(account);
}
}
注意:我们在LoginServcie是通过AuthenticationManager的authenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。
package com.example.easynote.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
/**
* @Classname SecurityConfig
* @Description: SpringSecurity配置类
* @Author: zengxi
* @CreateDate: 2024/1/11 21:07
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception
{
return super.authenticationManagerBean();
}
}
5、登录接口
此时我们通过调用LoginServcie中的login方法,来实现登录接口了
import com.example.easynote.common.controller.BaseController;
import com.example.easynote.common.domain.ResponseResult;
import com.example.easynote.common.domain.page.TableDataInfo;
import com.example.easynote.common.service.LoginServcie;
import com.example.easynote.module.user.domain.User;
import com.example.easynote.module.user.domain.UserDTO;
import com.example.easynote.module.user.service.UserService;
import com.github.pagehelper.PageInfo;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
/**
* @Classname UserController
* @Description: UserController
* @Author: zengxi
* @CreateDate: 2023/11/28 19:14
*/
@RestController
@RequestMapping("/user")
public class UserController extends BaseController {
@Resource
UserService userService;
@Resource
LoginServcie loginServciel;
@PostMapping("/login")
public ResponseResult login(@RequestBody UserDTO user) {
String token = loginServciel.login(user.getAccount(), user.getPassword());
return ResponseResult.success(token);
}
}
此时我们访问登录接口时发现,接口无返回结果,请求状态403,并且后端项目日志也无任何输出
这是SpringSecurity将我们的登录接口拦截了,只需在SecurityConfig,放行登录接口即可
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* anyRequest | 匹配所有请求路径
* access | SpringEl表达式结果为true时可以访问
* anonymous | 匿名可以访问
* denyAll | 用户不能访问
* fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录)
* hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问
* hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问
* hasAuthority | 如果有参数,参数表示权限,则其权限可以访问
* hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
* hasRole | 如果有参数,参数表示角色,则其角色可以访问
* permitAll | 用户可以任意访问
* rememberMe | 允许通过remember-me登录的用户访问
* authenticated | 用户登录后可访问
*/
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception
{
httpSecurity
// CSRF禁用,因为不使用session
.csrf().disable()
// 认证失败处理类
// .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 过滤请求
.authorizeRequests()
// 对于登录login 注册register 验证码captchaImage 允许匿名访问
.antMatchers("user/login", "/user/register", "/captchaImage").anonymous()
.antMatchers(
HttpMethod.GET,
"/",
"/*.html",
"/**/*.html",
"/**/*.css",
"/**/*.js",
"/profile/**"
).permitAll()
.antMatchers("/swagger-ui.html").anonymous()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated()
.and()
.headers().frameOptions().disable();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception
{
return super.authenticationManagerBean();
}
}
此时我们再访问登录接口,SpringSecurity放行了,但是报了如下错误
这是由于在Spring Security 5.xx版本中新增了加密方式的校验。之前版本中的NoOpPasswordEncoder被DelegatingPasswordEncoder取代了,由于在新建用户的时候,没有指定密码的加密方式。但是现有版本的Spring Security会对前端传来的密码进行检查,检验密码格式是否符合格式**{id}encodedPassword**,在DelegatingPasswordEncoder源码中给出了不同加密的样例,都是**{id}encodedPassword**的格式:
// {bcrypt}:BCrypt强哈希方法
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
// {noop}:无加密
{noop}password
// {PBKDF2}:PBKDF2加密
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
// {scrypt}:scrypt加密
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
// {sha256}:sha256加密
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0
解决方法:
一、修改数据库密码存储方式
将数据入库时,将存储的密码改为如下格式:
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
{noop}password
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0
二、修改UserDetails实现中的获取密码方法
import com.example.easynote.module.user.domain.User;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
/**
* @Classname LoginUser
* @Description: 实现UserDetails
* @Author: zengxi
* @CreateDate: 2024/1/11 20:35
*/
@Data
public class LoginUser implements UserDetails {
@Override
public String getPassword() {
// return "{noop}"+user.getPassword();
// return "{pbkdf2}"+user.getPassword();
// return "{scrypt}"+user.getPassword();
// return "{sha256}"+user.getPassword();
return "{bcrypt}"+user.getPassword();
}
}
三、修改SecurityConfig,SpringSecurity配置类(推荐)
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @Classname SecurityConfig
* @Description: SpringSecurity配置类
* @Author: zengxi
* @CreateDate: 2024/1/11 21:07
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 密码加密方式
*/
@Bean
public PasswordEncoder bCryptPasswordEncoder()
{
//不使用密码加密
//return NoOpPasswordEncoder.getInstance();
//使用默认的BCryptPasswordEncoder加密方案
return new BCryptPasswordEncoder();
//strength=10,即密钥的迭代次数(strength取值在4~31之间,默认为10)
//return new BCryptPasswordEncoder(10);
//利用工厂类PasswordEncoderFactories实现,工厂类内部采用的是委派密码编码方案.
//return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
6、注册接口
注册逻辑
**注意:**注册使用的密码加密方式,要和之前配置的加密方式一致
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.easynote.common.utils.StringUtils;
import com.example.easynote.module.user.domain.User;
import com.example.easynote.module.user.domain.UserDTO;
import com.example.easynote.module.user.mapper.UserMapper;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import javax.annotation.Resource;
import java.util.Objects;
/**
* @Classname RegisterService
* @Description: 注册功能逻辑实现
* @Author: zengxi
* @CreateDate: 2024/1/12 14:13
*/
@Service
public class RegisterService {
@Resource
UserMapper userMapper;
public String register(UserDTO userDTO) {
String msg = "", account = userDTO.getAccount(), password = userDTO.getPassword();
LambdaQueryWrapper<User> userLambdaQueryWrapper = new LambdaQueryWrapper();
userLambdaQueryWrapper.eq(User::getAccount, account);
if (StringUtils.isEmpty(account)) {
msg = "账户不能为空";
} else if (StringUtils.isEmpty(password)) {
msg = "密码不能为空";
} else if (Objects.nonNull(userMapper.selectOne(userLambdaQueryWrapper))) {
msg = "注册用胡" + account + "失败,该用户已存在";
} else {
User user = new User();
user.setAccount(account);
// 对密码进行加密
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encodePassword = passwordEncoder.encode(password);
user.setPassword(encodePassword);
user.setNickname(userDTO.getNickname());
user.setEmail(userDTO.getEmail());
user.setPhone(userDTO.getPhone());
user.setAvatar(userDTO.getAvatar());
user.setIntroduction(userDTO.getIntroduction());
if (userMapper.insert(user) > 0) {
msg = "注册成功";
} else {
msg = "注册失败,请联系系统管理人员";
}
}
return msg;
}
}
接口
@PostMapping("/register")
public ResponseResult register(@RequestBody UserDTO user) {
String msg = registerService.register(user);
return ResponseResult.success(msg);
}
认证
在有些情况下,我们需要用户登录后,才能访问某些接口。这时候就需要自定义一个过滤器,这个过滤器会去获取请求头中的token,对token进行解析。
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Resource
private JwtUtils jwtUtils;
@Resource
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = this.getTokenFromRequest(request);
if (StringUtils.isNotEmpty(token)) {
// 从 token 获取 username
String username = jwtUtils.getUsernameFromToken(token);
boolean tokenExpired = jwtUtils.isTokenExpired(jwtUtils.parseToken(token));
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (StringUtils.isNotNull(userDetails) && StringUtils.isNull(SecurityUtils.getAuthentication())) {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 设置用户到当前线程,以保证后续操作需要用户时能直接获取到用户信息
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
filterChain.doFilter(request, response);
}
/**
* @description: 截取token
* @author ZengXi
* @date 2024/7/24
* @param request
* @return java.lang.String
*/
private String getTokenFromRequest(HttpServletRequest request){
String bearerToken = request.getHeader("Authorization");
if(StringUtils.isNotEmpty(bearerToken) && bearerToken.startsWith("Bearer ")){
return bearerToken.substring(7, bearerToken.length());
}
return bearerToken;
}
}
然后将过滤器添加到SecurityConfig
中
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* token认证过滤器
*/
@Resource
private JwtAuthenticationTokenFilter authenticationTokenFilter;
/**
* 跨域过滤器
*/
@Resource
private CorsFilter corsFilter;
/**
* 强散列哈希加密实现
*/
@Bean
public PasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* anyRequest | 匹配所有请求路径
* access | SpringEl表达式结果为true时可以访问
* anonymous | 匿名可以访问
* denyAll | 用户不能访问
* fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录)
* hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问
* hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问
* hasAuthority | 如果有参数,参数表示权限,则其权限可以访问
* hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
* hasRole | 如果有参数,参数表示角色,则其角色可以访问
* permitAll | 用户可以任意访问
* rememberMe | 允许通过remember-me登录的用户访问
* authenticated | 用户登录后可访问
*/
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// CSRF禁用,因为不使用session
.csrf().disable()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 过滤请求
.authorizeRequests()
// 对于登录login 登出logout 注册register 验证码captchaImage 允许匿名访问
.antMatchers("/api/user/login", "/api/user/logout", "/api/user/register", "/captchaImage").anonymous()
.antMatchers(
HttpMethod.GET,
"/",
"/*.html",
"/**/*.html",
"/**/*.css",
"/**/*.js",
"/profile/**"
).permitAll()
.antMatchers("/swagger-ui.html").anonymous()
// .antMatchers("/api/**").permitAll() // 临时用于调试
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated()
.and()
.headers().frameOptions().disable();
// httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
// 添加JWT filter
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 将JWT filter和LogoutFilter添加到CORS filter之前
httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
如果还想要自定义认证失败后怎么处理,可以实现AuthenticationEntryPoint
/**
* 认证失败处理类 返回未授权
*
* @author zengxi
*/
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable
{
private static final long serialVersionUID = -8870718413537077606L;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
throws IOException {
int code = HttpStatus.UNAUTHORIZED;
String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI());
String jsonStr = JSONUtil.toJsonStr(ResponseResult.error(code, msg));
ServletUtils.renderString(response, jsonStr);
}
}
然后将AuthenticationEntryPointImpl
添加到SecurityConfig
中
/**
* 认证失败处理类
*/
@Resource
private AuthenticationEntryPointImpl unauthorizedHandler;
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// CSRF禁用,因为不使用session
.csrf().disable()
// 认证失败处理类
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 过滤请求
.authorizeRequests()
// 对于登录login 登出logout 注册register 验证码captchaImage 允许匿名访问
.antMatchers("/api/user/login", "/api/user/logout", "/api/user/register", "/captchaImage").anonymous()
.antMatchers(
HttpMethod.GET,
"/",
"/*.html",
"/**/*.html",
"/**/*.css",
"/**/*.js",
"/profile/**"
).permitAll()
.antMatchers("/swagger-ui.html").anonymous()
// .antMatchers("/api/**").permitAll() // 临时用于调试
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated()
.and()
.headers().frameOptions().disable();
// httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
// 添加JWT filter
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 将JWT filter和LogoutFilter添加到CORS filter之前
httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
}
授权
在SpringSecurity中,会使用默认的拦截器FilterSecurityInterceptor来进行权限校验。
在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然
后获取其中的权限信息,最后判断当前用户是否拥有访问当前资源所需的权限。
因此在项目中只需要把当前登录用户的权限信息也存入Authentication。
然后设置我们的资源所需要的权限即可。
原文链接:https://blog.csdn.net/qq_44749491/article/details/131469886
1、开启配置
开启配置需要再在SpringSecurity
的配置类中增加如下注解:
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
2、封装权限信息
在LoginUser
中添加permissions
、authorities
,并重写getAuthorities
方法
/**
* @Classname LoginUser
* @Description: 实现UserDetails
* @Author: zengxi
* @CreateDate: 2024/1/11 20:35
*/
@Data
public class LoginUser implements UserDetails {
private static final long serialVersionUID = 1L;
private User user;
List<String> permissions;
public LoginUser(User user, List<String> permissions){
this.user = user;
this.permissions = permissions;
}
//存储SpringSecurity所需要的权限信息的集合
@JsonIgnore
private List<GrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if (authorities != null){
return authorities;
}
//把permissions中字符串类型的权限信息转换成GrantedAuthority对象存入authorities中
authorities = permissions.stream().
map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
return authorities;
}
}
在UserDetailsServiceImpl
中的loadUserByUsername
方法中添加获取用户权限的代码
@Override
public UserDetails loadUserByUsername(String account) throws UsernameNotFoundException {
// 根据用户名查询用户信息
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getAccount,account);
User user = userMapper.selectOne(wrapper);
// 如果查询不到数据就通过抛出异常来给出提示
if(Objects.isNull(user)){
log.info("登录用户:{} 不存在",account);
throw new RuntimeException("登录用户:" + account + "不存在");
}
// 获取用户权限列表,这里只是为了测试,在真正的开发中,需要从数据库李查询
List<String> permissions = new ArrayList<>(Arrays.asList("permissions","ROLE_test"));
return new LoginUser(user, permissions);
}
3、测试请求类
@RestController
@RequestMapping("/api/test")
@Slf4j
public class TestController {
@PreAuthorize("hasAuthority('permissions')")
@GetMapping("/permissions")
public String permissions() {
return "permissions";
}
@PreAuthorize("hasAuthority('permissions2')")
@GetMapping("/permissions2")
public String permissions2() {
return "permissions2";
}
// hasAnyAuthority方法可以传入多个权限,只有用户有其中任意一个权限都可以访问对应资源。
@PreAuthorize("hasAnyAuthority('permissions','permissions2')")
@GetMapping("/permissions2")
public String permissions2() {
return "permissions2";
}
// hasRole,但是它内部会把我们传入的参数拼接上 ROLE_ 后再去比较
@PreAuthorize("hasRole('test')")
@GetMapping("/permissions3")
public String permissions3() {
return "permissions3";
}
// hasAnyRole,但是它内部会把我们传入的参数拼接上 ROLE_ 后再去比较
@PreAuthorize("hasAnyRole('admin','test')")
@GetMapping("/permissions3")
public String permissions3() {
return "permissions3";
}
}
4、自定义授权失败处理实现类
/**
* @Description 授权失败自定义类
* @Author ZengXi
* @Date 2024/8/28
*/
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN, "权限不足");
String jsonStr = JSONUtil.toJsonStr(result);
ServletUtils.renderString(response, jsonStr);
}
}
添加到SecurityConfig
中
@Resource
private AccessDeniedHandlerImpl accessDeniedHandler;
httpSecurity.exceptionHandling().accessDeniedHandler(accessDeniedHandler)