Spring Security
添加框架后,在不添加任何配置时,访问任何页面都会重定向到框架自带的login页面
框架提供了默认的用户名:user
与密码:启动时生成的UUID
Spring Security访问配置
配置类需要继承WebSecurityConfigurerAdapter
类,选择重写其中的 configure(HttpSecurity http)
方法
重写后访问页面将不需要登录,自带的登录和登出页面失效,post请求依然不可用
@Configuration
@Slf4j
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
}
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 配置授权访问的权限
http.authorizeRequests()
.anyRequest() // 选择任何请求必须匹配某些条件
.authenticated(); // 通过认证后才可以访问
http.formLogin();// 添加login和logout页面
}
@Configuration
@Slf4j
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
String[] urls = {"","/","/index.html"};
@Override
protected void configure(HttpSecurity http) throws Exception {
// 配置授权访问的权限
http.authorizeRequests()
.mvcMatchers(urls) // 匹配某些请求
.permitAll() // 不需要认证就可以访问
.anyRequest() // 选择任何请求必须匹配某些条件
.authenticated(); // 通过认证后
http.formLogin();// 添加login和logout页面
}
}
注意:访问权限的执行是从上至下,如果前面的配置已经匹配和通过条件,则下面的配置都不运行
上面完成的效果是,如果请求是访问首页,则不需要认证即可访问,其他请求则需要认证才可访问
临时自定义账号
使用自定义账号需要自定义类,并实现UserDetailsService接口,并确保此类是一个组件类,框架会基于此实现类的某些规则,来处理认证
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
log.debug("Spring Security自动调用了loadUserByUsername()方法,参数:{}", s);
// 假设正确的用户名是root,匹配的密码是1234
if (!"root".equals(s)) {
log.warn("用户名【{}】错误,将不会返回有效的UserDetails(用户详情)", s);
return null;
}
UserDetails userDetails = User.builder() // 构建者模式
.username("root") // 存入用户名
.password("1234") // 存入密码
.disabled(false) // 存入启用、禁用状态
.accountLocked(false) // 存入账号是否锁定的状态
.credentialsExpired(false) // 存入凭证是否过期的状态
.accountExpired(false) // 存入账号是否过期的状态
.authorities("这是一个临时的山寨权限,暂时没什么用") // 存入权限列表
.build(); // 执行构建,得到UserDetails类型的对象
log.debug("即将向Spring Security返回UserDetails类型的对象,返回结果:{}", userDetails);
return userDetails;
}
}
当项目中存在UserDetailsService
类型的组件时,登录框架自动使用登录页面表单中提交的用户名调用loadUserByUsername(String s)方法得到UserDetails
类型的对象,此对象中应该包含用户的相关信息,例如密码、账号状态等,接下来,Spring Security会自动的判断账号的状态,并使用登录表单提交过来的密码与UserDetails
中的密码进行对比,以决定此账号是否能够登录。
所以,当重写loadUserByUsername()
时,只需要实现“根据用户名返回匹配的用户详情”即可,至于此方法的调用、返回的结果如何用于判断是否能够成功登录,都是由Spring Security自动处理的!
Spring Security在验证登录时,要求密码必须经过加密处理,即:在loadUserByUsername()
方法中返回的UserDetails
中的密码必须是密文,即使你执意不加密,也必须明确的表示出来!
在SecurityConfiguration
类中,通过@Bean
方法来配置PasswordEncoder
,并返回某个密码编码器对象,Spring Security会自动使用它来验证密码,例如,可以使用NoOpPasswordEncoder
,表示“不对密码进行加密处理”,例如:
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
提示:当项目中存在UserDetailsService
类型的组件对象时,Spring Security框架不再提供默认的账号(用户名为user
,密码为启动时的UUID值的账号),所以,启动项目时也不会看到临时的UUID密码了。
以上用户信息(username、password等)在实际用途中可以修改为数据库中查找出来的信息,这样就能实现用户输入的数据和数据库中的信息进行比对进行验证
如果要比对数据库中的加密信息(例如:密码 存进数据库时需要加密处理),则需要弃用之前的不加密的密码编码器
目前最主流、安全性最高的加密算法 BCrypt 其主要特征有:
@Bean
public PasswordEncoder passwordEncoder() {
// return NoOpPasswordEncoder.getInstance();
return new BCryptPasswordEncoder();
}
解决post请求报403错误的问题
403错误是网站访问过程中,常见的错误提示。资源不可用,服务器理解客户的请求,但拒绝处理它。通常由于服务器上文件或目录的权限设置导致。
这是因为在Spring Security框架中为了解决伪造的跨域攻击
**伪造的跨域攻击:**此类攻击是基于“服务器对客户端的浏览器的信任”,例如,用户在浏览器的第1个选项卡中登录了,那么,在第2个、第3个等等同一个浏览器的其它选项卡中访问同样的服务器,也会被视为“已登录”的状态。所以,假设某个用户在浏览器的第1个选项卡中登录了网上银行,此用户在第2个选项卡中打开了另一个网站,此网站可能是恶意的网站(不是此前第1个选项卡的网上银行的网站),在恶意网站中隐藏了一个向网上银行的网站发起请求的链接,并自动发出了请求(比较典型的做法是将链接设置为<img>
标签的src
属性值,并隐藏此<img>
标签使之不显示),则会导致在第2个选项卡中的恶意网站被打开时,就自动的向网上银行发起了请求,而网上银行收到了请求后,会视为“已登录”的状态!
框架对此有一个解决方案:
服务器会要求客户端在发送表单时额外返回服务器提前生成的一串UUID,框架自带的表单中已经从服务器获取了该UUID,所以登陆时不会报错,但是自己编写的页面中没有此UUID所以会造成post请求报403错误,但是在前后端分离的项目中此类解决方法不适用
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();// 禁用跨站请求伪造解决方案
}
前后端分离的登录(认证)
由于框架自带的登录页面是存在服务器的,无法实现前后端分离的设计,所以需要先不启用框架自带的登录页面,改用自己编写页面发请求
接收一个新的请求时,如果需要使其不认证直接访问,需要在配置类中的“白名单”中添加
@Override
protected void configure(HttpSecurity http) throws Exception {
// 无需认证访问"白名单"
String[] urls = {
"",
"/",
"/index",
"/admins/login" // 登录页面
};
http.csrf().disable();
// 配置授权访问的权限
http.authorizeRequests()
.mvcMatchers(urls) // 匹配某些请求
.permitAll() // 不需要认证就可以访问
.anyRequest() // 选择任何请求必须匹配某些条件
.authenticated(); // 通过认证后
// http.formLogin();// 启用login和logout页面
}
@RestController
@RequestMapping("/admins")
public class AdminController {
@Autowired
IAdminService adminService;
@PostMapping("/login")
public JsonResult login(AdminLoginDTO adminLoginDTO){
System.out.println("调用service进行处理");
adminService.login(adminLoginDTO);
return JsonResult.ok();
}
}
@Service
public class AdminServiceImpl implements IAdminService {
@Autowired
private AuthenticationManager authenticationManager;
@Override
public void login(AdminLoginDTO adminLoginDTO) {
log.debug("开始处理验证登录的业务");
Authentication authentication = new UsernamePasswordAuthenticationToken(
adminLoginDTO.getUsername(),adminLoginDTO.getPassword()
);
authenticationManager.authenticate(authentication); // 调用认证管理器
log.debug("验证登录成功");
}
}
service验证过程中需要使用AuthenticationManager
接口,接口类型的对象可以通过Security配置类中重写父类方法得到 (方法上加@Bean注解使其自动添加到框架的容器中)
配置类中添加:
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
调用authenticationManager.authenticate(authentication)方法是需要一个参数,这个参数中包含的的是浏览器中输入的用户名和密码,验证时框架会调用之前写过的实现了UserDetailsService
接口的UserDetailsServiceImpl
类中的loadUserByUsername方法,而此方法中的userDetails对象则是数据库中的用户信息,此时就可以完成浏览器传递的数据的验证,通过就是认证成功!
401和403
401和403的主要区别在于
- 重点不同:401着重于认证,403着重于授权
- 返回对象不同:401通常由web服务器返回,403由web应用返回
- 场景不同:401表示用户未通过身份授权、验证,403表示用户可能通过了身份验证,但缺少指定权限
授权
在完成以上认证步骤后,可以提示认证成功,但是在做出其他访问时依然会提示403错误,这是因为上述认证步骤只是完成了数据的验证,没有将认证结果保存,所以需要进行相关操作
可以使用Spring Security中的SecurityContext来获取或者设置保存认证信息,SecurityContext
默认是基于Session的,所以,也符合Session的相关特征,例如默认的有效期(过期时间)。
在Spring Security中为每个客户端分配了一个SecurityContext
,并且Security会根据在SecurityContext中是否存在有效的Authenication
来判断是否已经通过认证
通过SecurityContextHolder
的静态方法getContext
方法即可得到当前客户端对应的SecurityContext对象
@Service
public class AdminServiceImpl implements IAdminService {
@Autowired
private AuthenticationManager authenticationManager;
@Override
public void login(AdminLoginDTO adminLoginDTO) {
log.debug("开始处理验证登录的业务");
Authentication authentication = new UsernamePasswordAuthenticationToken(
adminLoginDTO.getUsername(),adminLoginDTO.getPassword()
);
Authentication authenticationResult = // 新增
authenticationManager.authenticate(authentication);
log.debug("验证登录成功,即将向SecurityContext中存入Authentication"); // 新增
SecurityContext securityContext = SecurityContextHolder.getContext(); // 新增
securityContext.setAuthentication(authenticationResult); // 新增
}
}
之前调用的authenticationManager.authenticate()
方法有一个Authentication
类型的返回值,使用此类型可以.getContext
获取容器对象,调用容器对象的.setContext()
给容器中添加authenticationManager.authenticate()
方法的返回值
自定义UserDetails对象
如果当事人信息缺少逻辑相关的必须信息,例如 id和之前的enable的信息
创建一个类继承与UserDetail或它的子类,在构造方法中调用超类的全参构造方法
@ToString(callSuper = true)
public class AdminDetails extends User {
@Getter
private Long id;
public AdminDetails(Long id,String username, String password, boolean enabled,Collection<? extends GrantedAuthority> authorities) {
// 这里由于不需要关心boolean credentialsNonExpired, boolean accountNonLocked的值
// 所以直接传入一个固定的true
// ↓此数据为权限列表
super(username, password, enabled, true, true, true, authorities);
this.id = id;
}
}
同时,mapper
层的查询也需要与 AdminDetails
的数据保持一致(例如)id、enable权限列表信息
然后,调整UserDetailsServiceImpl
类中loadUserByUsername()
方法的返回结果,改为返回AdminDetails
类型的对象:
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
log.debug("Spring Security自动调用了loadUserByUsername()方法,参数:{}", s);
AdminLoginInfoVO loginInfo = adminMapper.getLoginInfoByUsername(s);
log.debug("根据用户名【{}】查询登录信息,结果:{}", s, loginInfo);
if (loginInfo == null) {
String message = "用户名不存在,将无法返回有效的UserDetails对象,则返回null";
log.warn(message);
return null;
}
log.debug("开始创建返回给Spring Security的UserDetails对象……");
// ========== 以下是新的代码,替换了原有的代码 ==========
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("这是一个临时的山寨权限,暂时没什么用"));
AdminDetails adminDetails = new AdminDetails(
loginInfo.getId(),
loginInfo.getUsername(),
loginInfo.getPassword(),
loginInfo.getEnable() == 1,
authorities
);
log.debug("即将向Spring Security返回UserDetails类型的对象,返回结果:{}", adminDetails);
return adminDetails;
}
此时从SecurityContext中获取到的Principal
(当事人)信息就是现在添加的AdminDetails中的所有数据了
修改之前controller
中的
@GetMapping("/getPrincipal")
// 注入的类型改为AdminDetails ↓↓↓↓↓↓↓↓↓↓↓↓
public String getPrincipal(@AuthenticationPrincipal AdminDetails adminDetails) {
log.debug("当事人用户id:{}",adminDetails.getId());
log.debug("当事人用户名是:{}",adminDetails.getUsername());
return "接收到【查询列表】请求,但未开发";
}
开启权限检查
修改之前UserDetailsServiceImpl
中的权限代码
@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private AdminMapper adminMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
log.debug("Spring Security自动调用了loadUserByUsername()方法,参数:{}", s);
AdminLoginInfoVO loginInfo = adminMapper.getLoginInfoByUsername(s);
log.debug("根据用户名【{}】查询登录信息,结果:{}", s, loginInfo);
if (loginInfo == null) {
String message = "用户名不存在,将无法返回有效的UserDetails对象,则返回null";
log.warn(message);
return null;
}
// ===== 以下是本次调整的代码 ======
// 将查询的权限列表信息添加
Collection<GrantedAuthority> authorities = new ArrayList<>();
for (String permission : loginInfo.getPermissions()) {
authorities.add(new SimpleGrantedAuthority(permission));
}
// 暂不关心后续的本次未调整的代码
}
}
经过以上调整后,在SecurityContext
中的Authentication
中的Principal
中的权限,就是从数据库中查出来的管理员的真实权限!
接下来,就可以利用Spring Security的检查权限的机制,需要先在配置类上开启“基于方法的权限检查”,例如:
@Slf4j
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) // 新增
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
// 暂不关心方法内部的代码
}
注意:以上注解是用于开启基于方法的权限检查,并不要求是哪个组件中的方法,也就是说,你可以把以上注解添加在项目中的任何方法上,例如Service中的方法、Controller中的方法,甚至其它组件中的方法,由于当前项目中,所有Service方法都是被Controller方法调用的,所以,推荐将检查权限的注解添加在Controller中的方法上。
然后,在AdminController
中:
@GetMapping("")
@PreAuthorize("hasAuthority('/ams/admin/read')") // 新增
public String list(@AuthenticationPrincipal @ApiIgnore
AdminDetails adminDetails) {
// 暂不关心方法内部的代码
}
至此,权限的配置已经完成!如果使用无权限的账号发起请求,服务器端会出现错误:
org.springframework.security.access.AccessDeniedException: 不允许访问
Session的问题
服务器端的程序通常是基于HTTP协议的,而HTTP协议是一种“无状态”的通信协议,所以,它并不能保存来访的客户端的状态,只是简单的“请求、响应”的处理而已!也就是说,当同一个客户端多次访问同一个服务器端时,服务器并不能识别来访的客户端就是前序曾经来访过的客户端!
在开发实践中,是需要识别客户端身份的,所以,在编程技术上,可以使用Session机制来解决此问题。
Session的本质是存储在服务器端的内存中的一个K-V结构的数据,服务器端会为每一个来访的客户端的首次访问分配一个Session ID(本质上是一个UUID值,如果客户端的请求中没有携带Session ID,则服务器端生成并发回给客户端,如果客户端的请求中已经携带Session ID,则服务器端不会生成)此Session ID就是客户端访问服务器端的Session数据时使用的Key,所以,每个客户端在服务器上都有一份对应的Session数据(K-V中的Value)。
由于Session是存储在服务器端的内存中的数据,内存是非常重要的,且容量相对较小的存储设备,所以,必须设置一些清除Session的机制,默认的典型的清除机制就是“超时自动清除”,也就是说,某个客户端在最后一次提交请求后的多长时间内(常见的超时时间是15分钟或30分钟)没有再次提交请求,则服务器端会自动清除此客户端对应的Session数据。
由于Session是存储在服务器端的内存中的数据,所以,必然存在一些缺点:
-
不适合存储大量的数据
-
- 无解
JWT
Token:令牌,票据
使用Token机制时,当客户端第1次向服务器提交请求时,或提交登录请求时,客户端直接发起请求,而服务器端会在验证登录成功后,生成此客户端对应的Token数据并响应到客户端,后续,客户端会携带此Token数据向服务器端发起请求,而服务器端会根据Token来识别客户端的身份。
在处理过程中,服务器端只需要检查Token、从Token中解析出客户端身份相关的数据即可,并不是必须在服务器端保存各Token数据,所以,Token可以设置较长时间的有效期,并不会长时间持续消耗服务器端的存储资源!所以,Token可以用于长时间表示用户的身份!
Token天生就适用于集群或分布式系统,因为各服务器端只需要具有相同的验证并解析Token的程序,就可以识别客户端的身份。
其实,Token的传输流程与Session ID基本上是相同的,最大的区别在于Session ID只是一个UUID数据,具有唯一性、随机性(不可预测性),但是,本身并不表示数据含义,而Token本身就是有数据含义的!
使用Jwt
关于JWT编程的工具包:https://jwt.io/libraries?language=Java
在选取jwt工具包时,建议选取github上获取星较多的,但其实任何jwt工具包都具备生成和解析的功能
<!-- JJWT(Java JWT) -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
生成并响应Jwt
在验证登录成功后,将之前存入SecurityConttext
的操作弃用,改为返回生成的Jwt
在AdminServiceImpl
中编写生成Jwt的代码如下
@Slf4j
@Service
public class AdminServiceImpl implements IAdminService {
@Value("${csmall.jwt.secret-key}")
private String secretKey;
@Value("${csmall.jwt.duration-in-minute}")
private long durationInMinute;
@Autowired
private AuthenticationManager authenticationManager;
@Override
// ↓将返回值改为String类型
public String login(AdminLoginDTO adminLoginDTO) {
log.debug("开始处理验证登录的业务");
Authentication authentication = new UsernamePasswordAuthenticationToken(
adminLoginDTO.getUsername(),adminLoginDTO.getPassword()
);
Authentication authenticationResult = authenticationManager.authenticate(authentication);
log.debug("验证登录成功,验证返回的结果是:{}",authenticationResult);
// -------------禁用此时存入SecurityContext的操作---------------------------
// log.debug("即将向SecurityContext中存入Authentication");
// SecurityContext securityContext = SecurityContextHolder.getContext();
// securityContext.setAuthentication(authenticationResult);
// 以下为新增
Map<String,Object> claims = new HashMap<>(); // 准备用户信息
AdminDetails adminDetails = (AdminDetails) authenticationResult.getPrincipal();
claims.put("id",adminDetails.getId());
claims.put("username",adminDetails.getUsername());
log.debug("jwt中的数据包含,{}",claims);
Date exp = new Date(System.currentTimeMillis() + durationInMinute * 1000 * 60);
String jwt = Jwts.builder()
.setHeaderParam("alg", "HS256") // 此处设置生成jwt的算法
.setHeaderParam("typ", "JWT") // 此处表明生成的为jwt
.setClaims(claims) // 此处为需要jwt中包含的数据
.setExpiration(exp) // 设置jwt的有效期
.signWith(SignatureAlgorithm.HS256, secretKey) // 指定验证签名
.compact();
log.debug("生成了JWT数据,并将返回此JWT数据:{}", jwt);
return jwt;
}
}
调整Controller
中的代码,将返回的jwt数据响应到客户端去
@PostMapping("/login")
public JsonResult login(AdminLoginDTO adminLoginDTO){
log.debug("开始处理【管理员登录】的请求,参数:{}", adminLoginDTO);
String jwt = adminService.login(adminLoginDTO);
return JsonResult.ok(jwt);// ←←←←←←←←
}
客户端接受到的数据如下
{
"state": 20000,
"data": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiZXhwIjoxNjgxNjI2ODQwLCJ1c2VybmFtZSI6InJvb3QifQ.9atdNiIRsGb6Ll4g58rLOBi5BoGQb1MoHFNsraCjwTo"
}
解析客户端携带的Jwt
客户端提交若干种不同的请求时,可能都需要携带JWT,在服务器端,处理若干种不同的请求之前也需要尝试接收并解析JWT,则应该使用**过滤器(Filter)**组件进行处理
过滤器是Java服务器端的组件中,最早接收到请求的组件,它执行在其它任何组件之前!在同一个项目中,允许存在若干个过滤器,形成过滤器链(Filter Chian),任何一个请求,必须被所有过滤器“放行”才可以被后续的组件(例如Controller等)进行处理!
在项目的根包下创建filter.JwtAuthorizationFilter
类,继承自OncePerRequestFilter
抽象类(将间接的实现Filter
接口),并在类上添加组件注解:
package cn.tedu.csmall.passport.filter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
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;
@Slf4j
@Component
public class JwtAuthorizationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
}
}
想要过滤器启用,还需要在Security配置类中将过滤器添加到过滤连中
@Slf4j
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
// 新增代码
@Autowired
private JwtAuthorizationFilter jwtAuthorizationFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 暂不关心其它代码
// 将自定义的JWT过滤器添加在Spring Security的UsernamePasswordAuthenticationFilter之前
http.addFilterBefore(jwtAuthorizationFilter,
UsernamePasswordAuthenticationFilter.class);
}
}
`接收客户端传递过来的jwt
`解析jwt
`将解析完成的数据存入SecurityContext共框架后续使用
所以在重写的doFileterInternal
方法中添加以下代码
/**
* 此过滤器主要完成三件事情(要启用过滤器还需要在配置类中自动装配过滤器对象)
* 1.通过header中获取用户传递来的Jwt信息
* 2.解析Jwt信息
* 3.将解析后的数据存储到SecurityContext中供框架处理后面的过滤
*/
@Slf4j
@Component
public class JwtAuthorizationFilter extends OncePerRequestFilter {
public static final int JWT_MIN_LENGTH = 113; // 携带的jwt最短长度
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 获取jwt信息
String jwt = request.getHeader("Authorization");
log.debug("接收用户携带的jwt :n{}",jwt);
// 判断用户携带的jwt是否是有效的
if(!StringUtils.hasText(jwt) || jwt.length() < JWT_MIN_LENGTH){
filterChain.doFilter(request,response);
return ;
}
// 尝试解析Jwt
String secretKey = "kU4jrFA3iuI5jn25u743kfDs7a8pFEwS54hm";
Claims claims = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
Long id = claims.get("id", Long.class);
String username = claims.get("username", String.class);
log.debug("从JWT中解析得到的管理员ID:{}", id);
log.debug("从JWT中解析得到的管理员用户名:{}", username);
// 将解析后的信息存储到SecurityContext中
//准备当事人信息
LoginPrincipal loginPrincipal = new LoginPrincipal();
loginPrincipal.setId(id);
loginPrincipal.setUsername(username);
Object principal = loginPrincipal;
// 凭证,应该为null
Object credentials = null;
// 权限列表
Collection<SimpleGrantedAuthority> attributes = new ArrayList<>();
attributes.add(new SimpleGrantedAuthority("假数据"));// 暂时不处理权限相关的问题
// 将信息添加到Authentication对象中
Authentication authentication = new UsernamePasswordAuthenticationToken(principal,credentials,attributes);
SecurityContext securityContext = SecurityContextHolder.getContext();
// 最后存入SecurityContext中
securityContext.setAuthentication(authentication);
filterChain.doFilter(request,response);
}
}
向SecurityContext中添加的Authentication对象需要三个参数,第一个为当事人信息,第二个为凭证,第三个为权限列表
由于当事人信息需要基本包含用户id和用户名username,而Authentication
中的Principal
的类型是Object
,所以,你可以使用任何类型的数据作为当事人,并且,在需要获取当事人信息时,添加@AuthenticationPrincipal
注解的参数也是你自行决定的当事人类型。
在项目的根包下创建security.LoginPrincipal
类型,用于封装当事人信息:
@Data
public class LoginPrincipal implements Serializable {
/**
* 当事人ID
*/
private Long id;
/**
* 当事人用户名
*/
private String username;
}
此时已经基本能完成解析Jwt的操作,由于没有处理权限相关需要将Controller中的权限代码暂时禁用
// 测试使用
@GetMapping("/getPrincipal")
// @PreAuthorize("hasAuthority('/ams/admin/read')") 暂时禁用
public String getPrincipal(@AuthenticationPrincipal @ApiIgnore LoginPrincipal loginPrincipal) {
log.debug("当事人用户id:{}",loginPrincipal.getId());
log.debug("当事人用户名是:{}",loginPrincipal.getUsername());
return "接收到【查询列表】请求,但未开发";
}
如果登录成功应该返回
{
"state": 20000,
"data": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MywiZXhwIjoxNjgwOTM4MjU5LCJ1c2VybmFtZSI6ImxpdWNhbmdzb25nIn0.IfnaR3S5wBDyxqaQMRxcr_te8tDV3zjvzRSRY8Njghg"
}
将返回的jwt添加到api测试文档中的全局参数中,用于过滤器解析使用
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MtmszR7o-1680855267814)(Spring%20Security.assets/image-20230407152132722.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d3JzRNQm-1680855267815)(Spring%20Security.assets/image-20230407152641651.png)]
关于权限
当数据需要输出且后续还需要读取时,可能会涉及序列化的问题,因为,当数据离开内存,就不再具有“数据类型”的含义了,包括将数据直接转换成字符串(例如将某数据存入到JWT中),则后续希望将字符串还原成原本的类型时,可能是无法做到的!
业内用于解决序列化和反序列化问题的常见手段就是使用JSON,先将对象转换成JSON格式的字符串,后续,需要还原时,再将JSON格式的字符串反序列化为对象。
<!-- fastjson:实现对象与JSON的相互转换 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.75</version>
</dependency>
其实在之前的AdminServiceImpl.login()
中的adminDetails中就含有权限信息
调整AdminServiceImpl
中的代码,
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img–gc78mz22-1680855267815)(Spring%20Security.assets/1680835416819.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1tUPAzgz-1680855267816)(Spring%20Security.assets/1680835561590.png)]
// 测试使用
@GetMapping("/getPrincipal")
@PreAuthorize("hasAuthority('/ams/admin/read')") // 启用
public String getPrincipal(@AuthenticationPrincipal @ApiIgnore LoginPrincipal loginPrincipal) {
log.debug("当事人用户id:{}",loginPrincipal.getId());
log.debug("当事人用户名是:{}",loginPrincipal.getUsername());
return "接收到【查询列表】请求,但未开发";
}
完成后,可以重启项目,使用root
管理员登录,可以执行所有操作,使用其他管理员,部分操作是不允许的!(注意:更换登录的管理员后,在调试发送请求之前,需要更换为对应的JWT数据)
处理解析JWT时的异常
由于解析JWT是在过滤器中执行的,而过滤器是整个服务器端中最早接收到任何请求的组件,此时,其它组件尚未开始处理当前请求,所以,不可以使用“全局异常处理器”来处理解析JWT时的异常(全局异常处理器只能处理控制器抛出的异常),则只能使用try...catch
语法来处理异常!
/**
* 错误:JWT已过期
*/
ERR_JWT_EXPIRED(60000),
/**
* 错误:验证签名失败
*/
ERR_JWT_SIGNATURE(60100),
/**
* 错误:JWT格式错误
*/
ERR_JWT_MALFORMED(60200),
然后,调整JwtAuthorizationFilter
中解析JWT的代码片段:
// 客户端携带了基本有效的JWT,则尝试解析JWT
Claims claims = null;
response.setContentType("application/json; charset=utf-8;");
try {
claims = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
} catch (ExpiredJwtException e) {
String message = "您的登录信息已过期,请重新登录!";
log.warn(message);
JsonResult jsonResult = JsonResult.fail(ServiceCode.ERR_JWT_EXPIRED, message);
String jsonResultString = JSON.toJSONString(jsonResult);
PrintWriter writer = response.getWriter();
writer.println(jsonResultString);
writer.close();
return;
} catch (SignatureException e) {
String message = "非法访问!";
log.warn(message);
JsonResult jsonResult = JsonResult.fail(ServiceCode.ERR_JWT_SIGNATURE, message);
String jsonResultString = JSON.toJSONString(jsonResult);
PrintWriter writer = response.getWriter();
writer.println(jsonResultString);
writer.close();
return;
} catch (MalformedJwtException e) {
String message = "非法访问!";
log.warn(message);
JsonResult jsonResult = JsonResult.fail(ServiceCode.ERR_JWT_MALFORMED, message);
String jsonResultString = JSON.toJSONString(jsonResult);
PrintWriter writer = response.getWriter();
writer.println(jsonResultString);
writer.close();
return;
} catch (Throwable e) {
String message = "服务器忙,请稍后再次尝试!(开发过程中,如果看到此提示,请检查控制台的信息,并在JWT过滤器补充处理此异常)";
log.warn(message);
JsonResult jsonResult = JsonResult.fail(ServiceCode.ERR_UNKNOWN, message);
String jsonResultString = JSON.toJSONString(jsonResult);
PrintWriter writer = response.getWriter();
writer.println(jsonResultString);
writer.close();
}
处理未登录的错误
当客户端提交请求时没有携带JWT,请求的目标却是需要通过认证的资源,则服务器端默认会响应403
错误!
此问题需要在Spring Security的配置类中的configurer(HttpSecurity http)
方法中添加配置来解决:
// 处理“当客户端提交请求时没有携带JWT,请求的目标却是需要通过认证的资源”的问题
http.exceptionHandling().authenticationEntryPoint(new AuthenticationEntryPoint() {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
response.setContentType("application/json; charset=utf-8;");
String message = "未检测到登录信息,请登录!(在开发阶段,看到此提示时,请检查客户端是否携带了有效的JWT数据)";
log.warn(message);
JsonResult jsonResult = JsonResult.fail(ServiceCode.ERR_UNAUTHORIZED, message);
String jsonResultString = JSON.toJSONString(jsonResult);
PrintWriter writer = response.getWriter();
writer.println(jsonResultString);
writer.close();
}
});
sultString);
writer.close();
}
# 处理未登录的错误
当客户端提交请求时没有携带JWT,请求的目标却是需要通过认证的资源,则服务器端默认会响应`403`错误!
此问题需要在Spring Security的配置类中的`configurer(HttpSecurity http)`方法中添加配置来解决:
```java
// 处理“当客户端提交请求时没有携带JWT,请求的目标却是需要通过认证的资源”的问题
http.exceptionHandling().authenticationEntryPoint(new AuthenticationEntryPoint() {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
response.setContentType("application/json; charset=utf-8;");
String message = "未检测到登录信息,请登录!(在开发阶段,看到此提示时,请检查客户端是否携带了有效的JWT数据)";
log.warn(message);
JsonResult jsonResult = JsonResult.fail(ServiceCode.ERR_UNAUTHORIZED, message);
String jsonResultString = JSON.toJSONString(jsonResult);
PrintWriter writer = response.getWriter();
writer.println(jsonResultString);
writer.close();
}
});
以上就完成了全套的Spring Security的单点登录功能。
原文地址:https://blog.csdn.net/weixin_72718065/article/details/130014998
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.7code.cn/show_42674.html
如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱:suwngjj01@126.com进行投诉反馈,一经查实,立即删除!