Spring Boot Security (三)

Spring Boot Security (三)

之前的随笔(https://www.cnblogs.com/zolmk/p/14074227.html)简单的使用了Spring Boot Security,没有深入。

一、主要内容

这篇主要的应用场景为前后端分离,前端Vue,后端Spring Boot(WebFlux)
本文主要实现了以下几点:
1.使用JdbcUserDetailsManager或者InMemoryUserDetailsManager实现用户认证和用户账户修改
2.实现登入登出控制
3.实现资源权限管理
4.解决跨域(cors)问题
注意区分这两个单词:AuthenticationAuthorization(认证和授权),前者通过账号密码进行认证,后者通过token对请求进行授权。

二、实现步骤

2.1 添加依赖

在pom.xml中添加如下依赖项

<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> </dependencies>

2.2 Security 配置

这里需要在配置文件中开启 WebFluxSecurity、ReactiveMethodSecurity等、对登录登出的处理、vue跨域问题等。具体配置文件如下:
SecurityConfiguration.java

//your package; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.StrUtil; import com.fy.shopexample.utils.ResponseCode; import com.fy.shopexample.utils.ResponseUtil; import com.fy.shopexample.utils.ServerHttpResponseUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager; import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextImpl; import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler; import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler; import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler; import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler; import org.springframework.security.web.server.context.ServerSecurityContextRepository; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import java.util.Collections; import java.util.Objects; @Slf4j @Configuration @EnableWebFluxSecurity @EnableReactiveMethodSecurity public class SecurityConfiguration { @Bean public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http, AuthenticationRepository repository, UserDetailServiceImpl userDetailService) { // 设置用户信息Service和密码更新Service UserDetailsRepositoryReactiveAuthenticationManager authenticationManager = new UserDetailsRepositoryReactiveAuthenticationManager(userDetailService); // 给UserDetailServiceImpl设置ReactiveAuthenticationManager,来响应更新密码 userDetailService.setAuthenticationManager(authenticationManager); authenticationManager.setUserDetailsPasswordService(userDetailService); authenticationManager.setPasswordEncoder(new PasswordEncoderImpl()); /* 鉴权成功后的处理器 */ final ServerAuthenticationSuccessHandler successHandler = (webFilterExchange, authentication) -> { String token = IdUtil.simpleUUID(); repository.put(token ,authentication); return ServerHttpResponseUtil.print(webFilterExchange.getExchange().getResponse(), ResponseUtil.success(token)); }; /* 鉴权失败后的处理器 */ final ServerAuthenticationFailureHandler failureHandler = (webFilterExchange, exception) -> ServerHttpResponseUtil.print(webFilterExchange.getExchange().getResponse(), ResponseUtil.of(ResponseCode.AUTHENTICATION_FAIL)); /* 登出成功处理器 */ final ServerLogoutSuccessHandler logoutHandler = (exchange, authentication) -> { String token = exchange.getExchange().getRequest().getHeaders().getFirst("token"); if (StrUtil.isNotBlank(token)) { repository.delete(token); } return ServerHttpResponseUtil.print(exchange.getExchange().getResponse(), ResponseUtil.success()); }; /* 权限拒绝处理器 */ final ServerAccessDeniedHandler deniedHandler = (exchange, denied) -> ServerHttpResponseUtil.print(exchange.getResponse(), ResponseUtil.of(ResponseCode.AUTHENTICATION_FAIL)); /* Security上下文仓库 * 它的意义是根据token将当前用户的授权信息提取出来,供当前请求上下文使用。 * */ final ServerSecurityContextRepository securityContextRepository = new ServerSecurityContextRepository() { @Override public Mono<Void> save(ServerWebExchange exchange, SecurityContext context) { return Mono.empty(); } @Override public Mono<SecurityContext> load(ServerWebExchange exchange) { String token = exchange.getRequest().getHeaders().getFirst("token"); if (StrUtil.isNotBlank(token)) { Authentication authentication = repository.get(token); if (Objects.nonNull(authentication)) { SecurityContextImpl securityContext = new SecurityContextImpl(authentication); return Mono.just(securityContext); } } return Mono.empty(); } }; /* 解决跨域问题 */ http.cors(corsSpec -> { CorsConfiguration corsConfiguration = new CorsConfiguration(); corsConfiguration.setAllowedOriginPatterns(Collections.singletonList("*")); corsConfiguration.addAllowedHeader(CorsConfiguration.ALL); corsConfiguration.addAllowedMethod(CorsConfiguration.ALL); corsConfiguration.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", corsConfiguration); corsSpec.configurationSource(source); }) .csrf() .disable() /* 登录和登出状态管理 */ .formLogin(s -> { s.loginPage("/login"); s.authenticationSuccessHandler(successHandler); s.authenticationFailureHandler(failureHandler); }) .logout(s -> { s.logoutSuccessHandler(logoutHandler); }) .passwordManagement() .and() .authorizeExchange(authorize -> { authorize.pathMatchers("/signup", "/login", "/logout").permitAll() .pathMatchers("/user/**").hasAnyRole("USER", "ADMIN", "DBA") .pathMatchers("/admin/**").hasAnyRole("ADMIN", "DBA") .pathMatchers("/db/**") .hasRole("DBA") .anyExchange().denyAll(); }) .exceptionHandling(s -> { s.accessDeniedHandler(deniedHandler); }) .authenticationManager(authenticationManager) .securityContextRepository(securityContextRepository); return http.build(); } }

其中ResponseCode和ResponseUtil是我自己对响应的封装,不重要,ServerHttpResponseUtil内容如下,主要就是将响应对象写入到ServerHttpResponse中。

import cn.hutool.json.JSONUtil; import com.fy.shopexample.dto.Response; import org.springframework.http.server.reactive.ServerHttpResponse; import reactor.core.publisher.Mono; import java.nio.charset.StandardCharsets; public class ServerHttpResponseUtil { public static Mono<Void> print(ServerHttpResponse httpResponse, Response<?,?> response) { return httpResponse.writeWith(Mono.just(httpResponse.bufferFactory().wrap(JSONUtil.toJsonStr(response).getBytes(StandardCharsets.UTF_8)))); } }

Security配置部分就这些内容,主要是在ServerHttpSecurity类上进行操作,配置认证与鉴权的规则和一些处理事件。

2.3 用户认证流程

  • vue前端通过axios发起post请求到/login接口,认证管理器对账号密码做校验,如果验证正确,则调用ServerAuthenticationSuccessHandler中的方法,如果失败,调用ServerAuthenticationFailureHandler中的方法
  • 认证成功后,我们可以给用户生成一个token,服务器对token和用户的授权信息进行保存后,将其返回给前端。
  • 前端拿到该token后进行本地保存,之后的每次请求都在headers上附带token,服务器根据该token对用户进行授权。

服务器对token和用户授权信息保存的相关类如下:
AuthenticationRepository.java

import org.springframework.security.core.Authentication; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Repository; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * 用来储存登录令牌 **/ @Repository public class AuthenticationRepository { private final static Map<String, Authentication> MAP = new ConcurrentHashMap<>(); private final static Map<String, String> USERNAME_TOKEN = new ConcurrentHashMap<>(); public void put(String key, Authentication authentication) { MAP.put(key, authentication); Object user = authentication.getPrincipal(); String username; if (user instanceof String) { username = (String) user; } else if (user instanceof UserDetails) { username = ((UserDetails) user).getUsername(); } else { throw new RuntimeException("用户不存在"); } USERNAME_TOKEN.put(username, key); } public Authentication get(String key) { return MAP.get(key); } public String getToken(String username) { return USERNAME_TOKEN.get(username); } public void delete(String key) { MAP.remove(key); } public void deleteByUsername(String username) { MAP.remove(USERNAME_TOKEN.get(username)); } }

这里简单省事直接将其保存在了内存中,最好的办法是将其保存在redis等缓存中间件中。类中的方法很简单,不再赘述。

一般而言,后端服务器数据库中不允许保存用户的明文密码,只允许保存加密后的用户密码,因此就需要一个密码编码器。密码验证流程为:1.服务器收到前端传来的密码 2.服务器通过密码编码器对密码进行编码 3.验证数据库中保存的密码和编码后的密码是否匹配 4.处理比对结果。

密码编码器如下:
PasswordEncoderImpl.java

import cn.hutool.core.codec.Base64; import lombok.extern.slf4j.Slf4j; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; import java.util.Objects; /** **/ @Slf4j @Component public class PasswordEncoderImpl implements PasswordEncoder { @Override public String encode(CharSequence rawPassword) { return Base64.encode(rawPassword); } @Override public boolean matches(CharSequence rawPassword, String encodedPassword) { return Objects.equals(encodedPassword, encode(rawPassword)); } @Override public boolean upgradeEncoding(String encodedPassword) { return false; } }

UserDetailsService类,该类实现了ReactiveUserDetailsServiceReactiveUserDetailsPasswordService接口,提供密码修改和使用用户名加载用户的功能。内容如下:
UserDetailServiceImpl.java

import cn.hutool.core.codec.Base64; import lombok.extern.slf4j.Slf4j; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService; import org.springframework.security.core.userdetails.ReactiveUserDetailsService; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; import java.util.HashMap; import java.util.Map; import static cn.hutool.core.codec.Base64.encode; @Service @Slf4j public class UserDetailServiceImpl implements ReactiveUserDetailsService, ReactiveUserDetailsPasswordService { // 这里暂时使用了该类来管理 private final Map<String, UserDetails> map; private final PasswordEncoder passwordEncoder; private ReactiveAuthenticationManager authenticationManager; private AuthenticationRepository authenticationRepository; public UserDetailServiceImpl(PasswordEncoder passwordEncoder, AuthenticationRepository authenticationRepository) { this.map = new HashMap<>(); this.passwordEncoder = passwordEncoder; this.authenticationRepository = authenticationRepository; // 先初始化一个用户 // 这里由于前端传入的就是经过Base64编码后的密码,然后在PasswordEncoder又进行来编码,所以这里要进行两次编码 UserDetails userDetails = User.withUsername("admin").password(encode(encode("1234"))).roles("DBA").build(); this.map.put(userDetails.getUsername(), userDetails); } @Override public Mono<UserDetails> updatePassword(UserDetails user, String newPassword) { String encodePassword = passwordEncoder.encode(newPassword); UserDetails userDetails = User.withUserDetails(user).password(encodePassword).build(); this.map.put(user.getUsername(), userDetails); Mono<UserDetails> res = Mono.just(userDetails); if (this.authenticationManager != null) { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(user.getUsername(), newPassword); Mono<Authentication> mono = this.authenticationManager.authenticate(token); res = mono.map(authentication -> (UserDetails)authentication.getPrincipal()); } // 最后修改鉴权存储器 return res.doOnSuccess(u -> { this.authenticationRepository.deleteByUsername(u.getUsername()); }); } @Override public Mono<UserDetails> findByUsername(String username) { return Mono.just(map.get(username)); } public void setAuthenticationManager(ReactiveAuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; } }

2.4 用户授权

用户授权过程如下:
1.前端发送请求,并在headers中附带token,通过在SecurityConfiguration配置文件中设置ServerSecurityContextRepository
2.当请求来临时,通过抽取headers中的token,然后根据token从AuthenticationRepository中获取相应的Authentication
3.然后生成上下文SecurityContextImpl securityContext = new SecurityContextImpl(authentication),从而对当前请求进行授权。

2.5 修改密码

简单起见,我直接卸载了Controller层,具体代码如下:

// 这个在UserDetailsService中有实现 @Autowired private ReactiveUserDetailsPasswordService passwordService; @Autowired private PasswordEncoder passwordEncoder; @PostMapping(path = "/change-password") public Mono<Response<?,?>> changePassword(@RequestBody Map<String,String> map, Authentication authentication) { String newPassword = map.get("newPassword"); if (StrUtil.isBlank(newPassword)) { return Mono.just(ResponseUtil.fail()); } // 获取用户信息 UserDetails userDetails = (UserDetails) authentication.getPrincipal(); //将用户权限设置到当前的上下文中(不知道为什么直接通过SecurityContextHolder.getContext().getAuthentication()为空) SecurityContextHolder.getContext().setAuthentication(authentication); return passwordService.updatePassword(userDetails, newPassword).map(u -> { if (StrUtil.equals(passwordEncoder.encode(newPassword), u.getPassword())) { return ResponseUtil.success(); } return ResponseUtil.fail(); }); }

2.6 方法级权限控制

这部分主要用注解来进行控制,示例代码如下:

import com.fy.shopexample.dto.Response; import org.springframework.security.access.prepost.PreAuthorize; import reactor.core.publisher.Mono; import java.util.List; public interface UserService { @PreAuthorize("hasAnyRole('DBA')") Mono<Response<?,?>> listUsers(int pageNumber, int pageSize); }

这里使用了PreAuthorize注解,它的参数在IDEA中有自动完成提示,大概有以下几种:

  • hasAnyRole(roleList):该方法需要具有相应的角色才能调用(只需满足其中一个)
  • hasRole(role):同上
  • hasPermission():当前用户需要具有对应权限
  • hasAuthority(authorityList):当前用户需要获得对应授权(只需满足其中一个)
  • hasAnyAuthority(authority):同上

三、问题汇总

3.1 SecurityContextHolder.getContext().getAuthentication()获取不到当前用户

在Controller层,可以通过下面的方式获得:

@PostMapping(path = "/change-password") public Mono<Response<?,?>> changePassword(@RequestBody Map<String,String> map, Authentication authentication) { }

3.2 使用数据库实现UserDetails的最快方式

Spring Boot Security提供了JdbcUserDetailsManager类,该类实现了UserDetailsManagerUserDetailsService接口,可以很方便的存取和修改用户。相应的表结构,懒得去找,如果要用,在JdbcUserDetailsManager类中能得到。


__EOF__

本文作者ZOLMK
本文链接https://www.cnblogs.com/zolmk/p/17343417.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   zolmk  阅读(168)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
点击右上角即可分享
微信分享提示