网站首页 > 文章精选 正文
Spring Security是用于解决认证与授权的框架。
//pom文件添加依赖
<dependencies>
<!-- Spring Boot Web:支持Spring MVC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Security:处理认证与授权 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
调整完成后,即可启动项目,在启动的日志中,可以看到类似以下内容:
//Spring Security有默认登录的账号和密码,密码是随机的,每次启动项目都会不同。
Using generated security password: 2abb9119-b5bb-4de9-8584-9f893e4a5a92
Spring Security默认要求所有的请求都是必须先登录才允许的访问,可以使用默认的用户名`user`和自动生成的随机密码来登录。在测试登录时,在浏览器访问当前主机的任意网址都可以(包括不存在的资源),会自动跳转到登录页(是由Spring Security提供的,默认的URL是:http://localhost:8080/login),当登录成功后,会自动跳转到此前访问的URL(跳转登录页之前的URL),另外,还可以通过 http://localhost:8080/logout 退出登录。
Spring Security的依赖项中包括了Bcrypt算法的工具类,Bcrypt是一款非常优秀的密码加密工具,适用于对需要存储下来的密码进行加密处理。
public class BcryptPasswordEncoderTests {
private BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
@Test
public void testEncode() {
// 原文相同的情况,每次加密得到的密文都不同
for (int i = 0; i < 10; i++) {
String rawPassword = "123456";
String encodedPassword = passwordEncoder.encode(rawPassword);
System.out.println("rawPassword = " + rawPassword);
System.out.println("encodedPassword = " + encodedPassword);
}
// rawPassword = 123456
// encodedPassword = $2a$10$hI4wweFOGJ7FMduSmCjNBexbKFOjYMWl8h
// encodedPassword = $2a$10$rOwgZMpDvZ3Kn7CxHWiEbeC6bQMGtfX.VY
}
@Test
public void testMatches() {
String rawPassword = "123456";
String encodedPassword = "$2a$10$hI4wweFOGJ7FMduSmCjNBexbKFOjYMWl8h";
boolean matchResult = passwordEncoder.matches(rawPassword, encodedPassword);
System.out.println("match result : " + matchResult);
}
}
要在当前模块(`csmall-passport`)中实现此查询功能,需要:
- - [`csmall-passport`] 添加数据库编程的相关依赖
- - `mysql-connector-java`
- - `mybatis-spring-boot-starter`
- - `durid` / `druid-spring-boot-starter`
- - [`csmall-passport`] 添加连接数据库的配置信息
- - [`csmall-passport`] 创建`MybatisConfiguration`配置类,用于配置`@MapperScan`
- - [`csmall-passport`] 在配置文件中配置`mybatis.mapper-locations`属性,以指定XML文件的位置
- - [`csmall-pojo`] 创建`AdminLoginVO`类
- - [`csmall-passport`] 在`pom.xml`中添加对`csmall-pojo`的依赖
- - [`csmall-passport`] 在`src/main/java`下的`cn.celinf.csmall.passport`包下创建`mapper.AdminMapper.java`接口
- - [`csmall-passport`] 在接口中添加抽象方法:
- - 在`src/main/resources`下创建`mapper`文件夹,并在此文件夹下粘贴得到`AdminMapper.xml`
- - 在`AdminMapper.xml`中配置以上抽象方法映射的SQL查询:
要实现Spring Security通过数据库的数据来验证用户名与密码(而不是采用默认的`user`用户名和随机的密码),则在`cn.celinf.csmall.passport`包下创建`security.UserDetailsServiceImpl`类,实现`UserDetailsService`接口,并重写接口中的抽象方法:
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private AdminMapper adminMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
System.out.println("根据用户名查询尝试登录的管理员信息,用户名=" + s);
AdminLoginVO admin = adminMapper.getLoginInfoByUsername(s);
System.out.println("通过持久层进行查询,结果=" + admin);
if (admin == null) {
System.out.println("根据用户名没有查询到有效的管理员数据,将抛出异常");
throw new BadCredentialsException("登录失败,用户名不存在!");
}
System.out.println("查询到匹配的管理员数据,需要将此数据转换为UserDetails并返回");
UserDetails userDetails = User.builder()
.username(admin.getUsername())
.password(admin.getPassword())
.accountExpired(false)
.accountLocked(false)
.disabled(admin.getIsEnable() != 1)
.credentialsExpired(false)
.authorities(admin.getPermissions().toArray(new String[] {}))
.build();
System.out.println("转换得到UserDetails=" + userDetails);
return userDetails;
}
}
完成后,再配置密码加密器即可:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityConfiguration {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
重启项目,可以发现在启动过程中不再生成随机的密码值,在浏览器上访问此项目的任何URL,进入登录页,即可使用数据库中的管理员数据进行登录。
在Spring Security,默认使用Session机制存储成功登录的用户信息(因为HTTP协议是无状态协议,并不保存客户端的任何信息,所以,同一个客户端的多次访问,对于服务器而言,等效于多个不同的客户端各访问一次,为了保存用户信息,使得服务器端能够识别客户端的身份,必须采取某种机制),当下,更推荐使用Token或相关技术(例如JWT)来解决识别用户身份的问题。
JWT = JSON Web Token
它是通过JSON格式组织必要的数据,将数据记录在票据(Token)上,并且,结合一定的算法,使得这些数据会被加密,然后在网络上传输,服务器端收到此数据后,会先对此数据进行解密,从而得到票据上记录的数据(JSON数据),从而识别用户的身份,或者处理相关的数据。
其实,在客户端第1次访问服务器端时,是“空着手”访问的,不会携带任何票据数据,当服务器进行响应时,会将JWT响应到客户端,客户端从第2次访问开始,每次都应该携带JWT发起请求,则服务器都会收到请求中的JWT并进行处理。
要使用JWT,需要添加相关的依赖项,可以实现生成JWT、解析JWT的框架较多,目前,主流的JWT框架可以是`jjwt`:
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
//测试使用JWT:
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Header;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.junit.jupiter.api.Test;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JwtTests {
// 密钥
String secretKey = "celinfisgoodstudent";
@Test
public void testGenerateJwt() {
// Claims
Map<String, Object> claims = new HashMap<>();
claims.put("id", 9527);
claims.put("name", "星星");
// JWT的组成部分:Header(头),Payload(载荷),Signature(签名)
String jwt = Jwts.builder()
// Header:指定算法与当前数据类型
// 格式为: { "alg": 算法, "typ": "jwt" }
.setHeaderParam(Header.CONTENT_TYPE, "HS256")
.setHeaderParam(Header.TYPE, Header.JWT_TYPE)
// Payload:通常包含Claims(自定义数据)和过期时间
.setClaims(claims)
.setExpiration(new Date(System.currentTimeMillis() + 5 * 60 * 1000))
// Signature:由算法和密钥(secret key)这2部分组成
.signWith(SignatureAlgorithm.HS256, secretKey)
// 打包生成
.compact();
// eyJjdHkiOiJIUzI1NiIsInR5cCI6IkpXVCIsImFsZyI6IkhTMjU2In0.e1MjcsImV4
System.out.println(jwt);
}
@Test
public void testParseJwt() {
String jwt = "eyJjdHkiOiJIUzI1NiIsInR5cCI6IkpXVCIsImFsZyI6IkhTMjU2In0.e1MjcsImV4";
Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();
Object id = claims.get("id");
Object name = claims.get("name");
System.out.println("id=" + id);
System.out.println("name=" + name);
}
}
JWT使用的异常
- io.jsonwebtoken.ExpiredJwtException(JWT数据过期时,异常)
- io.jsonwebtoken.MalformedJwtException(解析失败(数据有误)时,异常)
- io.jsonwebtoken.SignatureException(密钥不一致时,异常)
Spring Security中使用JWT,至少需要:
不能让Spring Security按照原有模式来处理登录(原有模式中,登录成功后,自动装用户信息存储到Session中,且跳转页面),需要
- - 需要自动装配`AuthenticationManager`对象
- - 使得`SecurityConfiguration`配置类继承自`WebSecurityConfigurerAdapter`类,重写其中的`xx`方法,在此方法中直接调用父级方法即可,并在此方法上添加`@Bean`注解
- - 创建`AdminLoginDTO`类,此类中应该包含用户登录时需要提交的用户名、密码
- - 创建`IAdminService`接口
- - 在`IAdminService`接口中添加登录的抽象方法(String login(AdminLoginDTO adminLoginDTO);)
- -创建`AdminServiceImpl`类,实现以上接口
- - 在实现过程中,调用`AuthenticationManager`实现认证,当认证成功后,生成JWT并返回
- - 创建`AdminController`类,在类中处理登录请求
- - 在`SecurityConfiguration`中配置Spring Security,对特定的请求进行放行(默认所有请求都必须先登录)
相关代码:
package cn.celinf.csmall.passport.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
//该写法不支持spring boot 2.7之后版本
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 禁用防跨域攻击
http.csrf().disable();
// URL白名单
String[] urls = {
"/admins/login"
};
// 配置各请求路径的认证与授权
http.authorizeRequests() // 请求需要授权才可以访问
.antMatchers(urls) // 匹配一些路径
.permitAll() // 允许直接访问(不需要经过认证和授权)
.anyRequest() // 匹配除了以上配置的其它请求
.authenticated(); // 都需要认证
}
}
/**【注意】spring boot 2.7以后的写法
* SpringSecurity 5.4.x以上新用法配置
* 为避免循环依赖,仅用于配置HttpSecurity
*/
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
//省略HttpSecurity的配置
return httpSecurity.build();
}
}
package cn.celinf.csmall.pojo.dto;
import lombok.Data;
import java.io.Serializable;
@Data
public class AdminLoginDTO implements Serializable {
private String username;
private String password;
}
package cn.celinf.csmall.passport.service;
import cn.celinf.csmall.pojo.dto.AdminLoginDTO;
public interface IAdminService {
String login(AdminLoginDTO adminLoginDTO);
}
package cn.celinf.csmall.passport.service;
import cn.celinf.csmall.pojo.dto.AdminLoginDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
@Service
public class AdminServiceImpl implements IAdminService {
@Autowired
private AuthenticationManager authenticationManager;
@Override
public String login(AdminLoginDTO adminLoginDTO) {
// 准备被认证数据
Authentication authentication
= new UsernamePasswordAuthenticationToken(
adminLoginDTO.getUsername(), adminLoginDTO.getPassword());
// 调用AuthenticationManager验证用户名与密码
// 执行认证,如果此过程没有抛出异常,则表示认证通过,如果认证信息有误,将抛出异常
authenticationManager.authenticate(authentication);
// 如果程序可以执行到此处,则表示登录成功
// 生成此用户数据的JWT
String jwt = "This is a JWT."; // 临时
return jwt;
}
}
package cn.celinf.csmall.passport.controller;
import cn.celinf.csmall.passport.service.IAdminService;
import cn.celinf.csmall.pojo.dto.AdminLoginDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping(value = "/admins", produces = "application/json; charset=utf-8")
public class AdminController {
@Autowired
private IAdminService adminService;
// http://localhost:8080/admins/login?username=root&password=123456
@RequestMapping("/login")
public String login(AdminLoginDTO adminLoginDTO) {
String jwt = adminService.login(adminLoginDTO);
return jwt;
}
}
以上全部完成后,启动项目,打开浏览器,可以通过 http://localhost:8080/admins/login?username=root&password=123456 这类URL测试登录,使用数据库中的用户名和密码进行尝试。
当通过以上URL进行访问时,其内部过程大概是:
- - Spring Security的相关配置会进行URL的检查,来判断是否允许访问此路径
- - 所以,需要在`SecurityConfiguration`中将以上路径设置为白名单
- - 如果没有将以上路径配置到白名单,将直接跳转到登录页,因为默认所有请求都必须先登录
- - 由`AdminController`接收到请求后,调用了`IAdminService`接口的实现类对象来处理登录
- - `IAdminService`接口的实现是`AdminServiceImpl`
- - 在`AdminServiceImpl`中,调用了`AuthenticationManager`处理登录的认证
- - `AuthenticationManager`对象调用`authenticate()`方法进行登录处理
- - 内部实现中,会自动调用`UserDetailsService`实现对象的`loadUserByUsername()`方法以获取用户信息,并自动完成后续的认证处理(例如验证密码是否正确),所以,在步骤中,具体执行的是`UserDetailsServiceImpl`类中重写的方法,此方法返回了用户信息,Spring Security自动验证,如果失败(例如账号已禁用、密码错误等),会抛出异常
- - 以上调用的`authenticate()`方法如果未抛出异常,可视为认证成功,即登录成功
- - 当登录成功时,应该返回此用户的JWT数据(暂时未实现)
此前,在处理登录的业务中,当视为登录成功时,返回的字符串并不是JWT数据,则应该将此数据改为必要的JWT数据。
@Service
public class AdminServiceImpl implements IAdminService {
// ===== 原有其它代码 =====
/**
* JWT数据的密钥
*/
private String secretKey = "celinfisgoodstudent";
@Override
public String login(AdminLoginDTO adminLoginDTO) {
// ===== 原有其它代码 =====
// 如果程序可以执行到此处,则表示登录成功
// 生成此用户数据的JWT
// Claims
User user = (User) authenticate.getPrincipal();
System.out.println("从认证结果中获取Principal=" + user.getClass().getName());
Map<String, Object> claims = new HashMap<>();
claims.put("username", user.getUsername());
claims.put("permissions", user.getAuthorities());
System.out.println("即将向JWT中写入数据=" + claims);
// JWT的组成部分:Header(头),Payload(载荷),Signature(签名)
String jwt = Jwts.builder()
// Header:指定算法与当前数据类型
// 格式为: { "alg": 算法, "typ": "jwt" }
.setHeaderParam(Header.CONTENT_TYPE, "HS256")
.setHeaderParam(Header.TYPE, Header.JWT_TYPE)
// Payload:通常包含Claims(自定义数据)和过期时间
.setClaims(claims)
.setExpiration(new Date(System.currentTimeMillis() + 5 * 60 * 1000))
// Signature:由算法和密钥(secret key)这2部分组成
.signWith(SignatureAlgorithm.HS256, secretKey)
// 打包生成
.compact();
// 返回JWT数据
return jwt;
}
}
在控制器中,应该响应JSON格式的数据,将控制器中处理请求的方法的返回值类型改为`JsonResult<String>`,并调整返回值:
// http://localhost:8080/admins/login?username=root&password=123456
@RequestMapping("/login")
public JsonResult<String> login(AdminLoginDTO adminLoginDTO) {
String jwt = adminService.login(adminLoginDTO);
return JsonResult.ok(jwt);
}
此时,重启项目,在浏览器中,使用正确的用户名和密码访问,响应的结果例如:
{
"state":20000,
"message":null,
"data":"eyJjdHkiOiJIUzI1NiIsInR5cCI6IkpXVCIsImFsZyI6IkhTMjU2In0.
eyJwZXJtaXNzaW9ucyI6W3siYXV0aG9yaXR5IjoiL2Ftcy9hZG1pbi9kZWxldGUifSx7Im
F1dGhvcml0eSI6Ii9hbXMvYWRtaW4vcmVhZCJ9LHsiYXV0aG9yaXR5IjoiL2Ftcy9hZG
1pbi91cGRhdGUifSx7ImF1dGhvcml0eSI6Ii9wbXMvcHJvZHVjdC9kZWxldGUifSx7ImF1
dGhvcml0eSI6Ii9wbXMvcHJvZHVjdC9yZWFkIn0seyJhdXRob3JpdHkiOiIvcG1zL3Byb2R1
Y3QvdXBkYXRlIn1dLCJleHAiOjE2NTU0MzQwMzcsInVzZXJuYW1lIjoicm9vdCJ9.
8ZIfpxxjJlwNo-E3JhXwH4sZR0J5-FU-HAOMu1Tg-44"
}
注意:以上只是访问`/admins/login`时会执行所编写的流程(发送用户名和密码,得到含JWT的结果),并不代表真正意义的实现了“登录”!
登录的流程应该是:
- 客户端提交用户名和密码到服务器端 >>>
- 服务器端认证成功后响应JWT >>>
- 客户端在后续的请求中都携带JWT >>>
- 服务器端验证JWT来决定是否允许访问。
为了便于体现“客户端在后续的请求中都携带JWT”的操作,可以在项目中添加使用Knife4j。
当使用Knife4j时,需要在白名单中添加相关的放行资源路径,否则,Knife4j的页面将无法使用:
@Configuration
// extends WebSecurityConfigurerAdapter 不支持spring boot 2.7以上版本
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
// ===== 原有其它代码 =====
@Override
protected void configure(HttpSecurity http) throws Exception {
// ===== 原有其它代码 =====
// URL白名单
String[] urls = {
"/admins/login",
"/doc.html", // 从本行开始,以下是新增
"/**/*.js",
"/**/*.css",
"/swagger-resources",
"/v2/api-docs",
"/favicon.ico"
};
// ===== 原有其它代码 =====
}
}
在后续的访问中,必须在请求中携带JWT数据, 服务器端才可以尝试解析此JWT数据,从而判断用户是否已登录或允许访问。
在规范的使用方式中,JWT数据必须携带在请求头(Request Header)的`Authorization`属性中。
按照以上规范,则服务器端在每次接收到请求后,首先,就应该先判断请求头中是否存在`Authorization`、`Authorization`的值是否有效等操作,通常,是通过过滤器来实现以上检查的。
在`csmall-passport`的根包下的`security`包下创建`JwtAuthenticationFilter`过滤器类,需要继承自`OncePerRequestFilter`类:
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
System.out.println("JwtAuthenticationFilter.doFilterInternal()");
}
}
所有的过滤器都必须注册后才可以使用,且同一个项目中允许存在多个过滤器,形成过滤器链,以上用于验证JWT的过滤器应该运行在Spring Security处理登录的过滤器之前,需要在自定义的`SecurityConfiguration`中的`configure()`方法中将以上自定义的过滤器注册在Spring Security的相关过滤器之前:
@Configuration
// extends WebSecurityConfigurerAdapter 不支持spring boot 2.7以上版本
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
// 新增
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
// ===== 原有其它代码 =====
@Override
protected void configure(HttpSecurity http) throws Exception {
// ===== 原有其它代码 =====
// 注册处理JWT的过滤器
// 此过滤器必须在Spring Security处理登录的过滤器之前
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
完成后,重启项目,无论对哪个路径发出请求,在控制台都可以看出输出了过滤器中的输出语句内容,并且,在浏览器将显示一片空白。
关于`JwtAuthenticationFilter`,它需要实现:
- - 尝试从请求头中获取JWT数据
- - 如果无JWT数据,应该直接放行,Spring Security还会进行后续的处理,例如白名单的请求将允许访问,其它请求将禁止访问
- - 如果存在JWT数据,应该尝试解析
- - 如果解析失败,应该视为错误,可以要求客户端重新登录,客户端就可以得到新的、正确的JWT,客户端在下一次提交请求时,使用新的JWT即可正确访问
- - 将解析得到的数据封装到`Authentication`对象中
- - Spring Security的上下文中存储的数据类型是`Authentication`类型
- - 为避免存入1次后,Spring Security的上下文中始终存在`Authentication`,在此过滤器执行的第一时间,应该清除上下文中的数据
package cn.celinf.csmall.passport.security;
import cn.celinf.csmall.common.web.JsonResult;
import cn.celinf.csmall.common.web.State;
import com.alibaba.fastjson.JSON;
import io.jsonwebtoken.*;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
/**
* JWT过滤器:从请求头的Authorization中获取JWT中存入的用户信息
* 并添加到Spring Security的上下文中
* 以致于Spring Security后续的组件(包括过滤器等)能从上下文中获取此用户的信息
* 从而验证是否已经登录、是否具有权限等
*/
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
/**
* JWT数据的密钥
*/
private String secretKey = "fgfdsfadsfadsafdsafdsfadsfadsfdsafdasfdsafdsafdsafds4rttrefds";
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
System.out.println("JwtAuthenticationFilter.doFilterInternal()");
// 清除Spring Security上下文中的数据
// 避免此前曾经存入过用户信息,后续即使没有携带JWT,在Spring Security仍保存有上下文数据
//(包括用户信息)
System.out.println("清除Spring Security上下文中的数据");
SecurityContextHolder.clearContext();
// 客户端提交请求时,必须在请求头的Authorization中添加JWT数据,这是当前服务器程
//序的规定,客户端必须遵守
// 尝试获取JWT数据
String jwt = request.getHeader("Authorization");
System.out.println("从请求头中获取到的JWT=" + jwt);
// 判断是否不存在jwt数据
if (!StringUtils.hasText(jwt)) {
// 不存在jwt数据,则放行,后续还有其它过滤器及相关组件进行其它的处理,例如未登录则
//要求登录等
// 此处不宜直接阻止运行,因为“登录”、“注册”等请求本应该没有jwt数据
System.out.println("请求头中无JWT数据,当前过滤器将放行");
filterChain.doFilter(request, response); // 继续执行过滤器链中后续的过滤器
return; // 必须
}
// 注意:此时执行时,如果请求头中携带了Authentication,日志中将输出,且不会有
//任何响应,因为当前过滤器尚未放行
// 以下代码有可能抛出异常的
// TODO 密钥和各个Key应该统一定义
String username = null;
String permissionsString = null;
try {
System.out.println("请求头中包含JWT,准备解析此数据……");
Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();
username = claims.get("username").toString();
permissionsString = claims.get("permissions").toString();
System.out.println("username=" + username);
System.out.println("permissionsString=" + permissionsString);
} catch (ExpiredJwtException e) {
System.out.println("解析JWT失败,此JWT已过期:" + e.getMessage());
JsonResult<Void> jsonResult = JsonResult.fail(
State.ERR_JWT_EXPIRED, "您的登录已过期,请重新登录!");
String jsonString = JSON.toJSONString(jsonResult);
System.out.println("响应结果:" + jsonString);
response.setContentType("application/json; charset=utf-8");
response.getWriter().println(jsonString);
return;
} catch (MalformedJwtException e) {
System.out.println("解析JWT失败,此JWT数据错误,无法解析:" + e.getMessage());
JsonResult<Void> jsonResult = JsonResult.fail(
State.ERR_JWT_MALFORMED, "获取登录信息失败,请重新登录!");
String jsonString = JSON.toJSONString(jsonResult);
System.out.println("响应结果:" + jsonString);
response.setContentType("application/json; charset=utf-8");
response.getWriter().println(jsonString);
return;
} catch (SignatureException e) {
System.out.println("解析JWT失败,此JWT签名错误:" + e.getMessage());
JsonResult<Void> jsonResult = JsonResult.fail(
State.ERR_JWT_SIGNATURE, "获取登录信息失败,请重新登录!");
String jsonString = JSON.toJSONString(jsonResult);
System.out.println("响应结果:" + jsonString);
response.setContentType("application/json; charset=utf-8");
response.getWriter().println(jsonString);
return;
} catch (Throwable e) {
System.out.println("解析JWT失败,异常类型:" + e.getClass().getName());
e.printStackTrace();
JsonResult<Void> jsonResult = JsonResult.fail(
State.ERR_INTERNAL_SERVER_ERROR, "获取登录信息失败,请重新登录!");
String jsonString = JSON.toJSONString(jsonResult);
System.out.println("响应结果:" + jsonString);
response.setContentType("application/json; charset=utf-8");
response.getWriter().println(jsonString);
return;
}
// 将此前从JWT中读取到的permissionsString(JSON字符串)转换成Collection
//<? extends GrantedAuthority>
List<SimpleGrantedAuthority> permissions
= JSON.parseArray(permissionsString, SimpleGrantedAuthority.class);
System.out.println("从JWT中获取到的权限转换成Spring Security要求的类型:" + permissions);
// 将解析得到的用户信息传递给Spring Security
// 获取Spring Security的上下文,并将Authentication放到上下文中
// 在Authentication中封装:用户名、null(密码)、权限列表
// 因为接下来并不会处理认证,所以Authentication中不需要密码
// 后续,Spring Security发现上下文中有Authentication时,就会视为已登录,甚至可以获取相关信息
Authentication authentication
= new UsernamePasswordAuthenticationToken(username, null, permissions);
SecurityContextHolder.getContext().setAuthentication(authentication);
System.out.println("将解析得到的用户信息传递给Spring Security");
// 放行
System.out.println("JwtAuthenticationFilter 放行");
filterChain.doFilter(request, response);
}
}
要使用Spring Security实现授权访问,首先,必须保证用户登录后,在Spring Security上下文中存在权限相关信息(目前,此项已完成,在`JwtAuthenticationFilter`的最后,已经存入权限信息)。
然后,需要在配置类上使用`@EnableGlobalMethodSecurity`注解开启“通过注解配置权限”的功能,所以,在`SecrutiyConfiguration`类上添加:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) // 新增
// extends WebSecurityConfigurerAdapter 不支持spring boot 2.7以上版本
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
// ===== 类中原有代码 =====
}
最后,在任何你需要设置权限的处理请求的方法上,通过`@PreAuthorize`注解来配置要求某种权限,例如:
@GetMapping("/hello")
@PreAuthorize("hasAuthority('/ams/admin/read')") // 新增
public String sayHello() {
return "hello~~~";
}
完成后,重启项目,使用具有`/ams/admin/read`权限的用户可以直接访问,不具有此权限的用户则不能访问(将出现403)。
学习记录,如有侵权请联系删除
猜你喜欢
- 2024-12-30 简单的使用SpringBoot整合SpringSecurity
- 2024-12-30 Spring Security 整合OAuth2 springsecurity整合oauth2+jwt+vue
- 2024-12-30 DeepSeek-Coder-V2震撼发布,尝鲜体验
- 2024-12-30 一个数组一行代码,Spring Security就接管了Swagger认证授权
- 2024-12-30 简单漂亮的(图床工具)开源图片上传工具——PicGo
- 2024-12-30 Spring Boot(十一):Spring Security 实现权限控制
- 2024-12-30 绝了!万字搞定 Spring Security,写得太好了
- 2024-12-30 SpringBoot集成Spring Security springboot集成springsecurity
- 2024-12-30 SpringSecurity密码加密方式简介 spring 密码加密
- 2024-12-30 Spring cloud Alibaba 从入门到放弃
- 最近发表
- 标签列表
-
- newcoder (56)
- 字符串的长度是指 (45)
- drawcontours()参数说明 (60)
- unsignedshortint (59)
- postman并发请求 (47)
- python列表删除 (50)
- 左程云什么水平 (56)
- 计算机网络的拓扑结构是指() (45)
- 稳压管的稳压区是工作在什么区 (45)
- 编程题 (64)
- postgresql默认端口 (66)
- 数据库的概念模型独立于 (48)
- 产生系统死锁的原因可能是由于 (51)
- 数据库中只存放视图的 (62)
- 在vi中退出不保存的命令是 (53)
- 哪个命令可以将普通用户转换成超级用户 (49)
- noscript标签的作用 (48)
- 联合利华网申 (49)
- swagger和postman (46)
- 结构化程序设计主要强调 (53)
- 172.1 (57)
- apipostwebsocket (47)
- 唯品会后台 (61)
- 简历助手 (56)
- offshow (61)