登录⽤户数据获取
SecurityContextHolder
Spring Security 会将登录⽤户数据保存在 Session 中。但是,为了使⽤⽅便, Spring Security 在此基础上还做了⼀些改进,其中最主要的⼀个变化就是线程绑定。当⽤户登录成功后,Spring Security 会将登录成功的⽤户信息保存到SecurityContextHolder 中。
SecurityContextHolder 中的数据保存默认是通过 ThreadLocal 来实现的,使⽤ ThreadLocal 创建的变量只能被当前线程访问,不能被其他线程访问和修改,也就是⽤户数据和请求线程绑定在⼀起。当登录请求处理完毕后, Spring Security 会将SecurityContextHolder 中的数据拿出来保存到 Session 中,同时将 SecurityContexHolder 中的数据清空。以后每当有请求到来时, Spring Security 就会先从 Session 中取出⽤户登录数据,保存到SecurityContextHolder 中,⽅便在该请求的后续处理过程中使⽤,同时在请求结束时将 SecurityContextHolder 中的数据拿出来保存到 Session 中,然后将SecurityContextHolder 中的数据清空。
实际上 SecurityContextHolder 中存储是 SecurityContext,在SecurityContext 中存储是 Authentication。
这种设计是典型的策略设计模式:
public class SecurityContextHolder {
public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
public static final String MODE_GLOBAL = "MODE_GLOBAL";
private static final String MODE_PRE_INITIALIZED = "MODE_PRE_INITIALIZED";
public static final String SYSTEM_PROPERTY = "spring.security.strategy";
private static String strategyName = System.getProperty(SYSTEM_PROPERTY);
private static SecurityContextHolderStrategy strategy;
private static int initializeCount = 0;
static {
initialize();
}
private static void initialize() {
initializeStrategy();
initializeCount++;
}
private static void initializeStrategy() {
if (MODE_PRE_INITIALIZED.equals(strategyName)) {
Assert.state(strategy != null, "When using " + MODE_PRE_INITIALIZED
+ ", setContextHolderStrategy must be called with the fully constructed strategy");
return;
}
if (!StringUtils.hasText(strategyName)) {
// Set default
strategyName = MODE_THREADLOCAL;
}
if (strategyName.equals(MODE_THREADLOCAL)) {
strategy = new ThreadLocalSecurityContextHolderStrategy();
return;
}
if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
return;
}
if (strategyName.equals(MODE_GLOBAL)) {
strategy = new GlobalSecurityContextHolderStrategy();
return;
}
// Try to load a custom strategy
try {
Class<?> clazz = Class.forName(strategyName);
Constructor<?> customStrategy = clazz.getConstructor();
strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();
}
catch (Exception ex) {
ReflectionUtils.handleReflectionException(ex);
}
}
/**
* Explicitly clears the context value from the current thread.
*/
public static void clearContext() {
strategy.clearContext();
}
/**
* Obtain the current <code>SecurityContext</code>.
* @return the security context (never <code>null</code>)
*/
public static SecurityContext getContext() {
return strategy.getContext();
}
/**
* Primarily for troubleshooting purposes, this method shows how many times the class
* has re-initialized its <code>SecurityContextHolderStrategy</code>.
* @return the count (should be one unless you've called
* {@link #setStrategyName(String)} or
* {@link #setContextHolderStrategy(SecurityContextHolderStrategy)} to switch to an
* alternate strategy).
*/
public static int getInitializeCount() {
return initializeCount;
}
/**
* Associates a new <code>SecurityContext</code> with the current thread of execution.
* @param context the new <code>SecurityContext</code> (may not be <code>null</code>)
*/
public static void setContext(SecurityContext context) {
strategy.setContext(context);
}
/**
* Changes the preferred strategy. Do <em>NOT</em> call this method more than once for
* a given JVM, as it will re-initialize the strategy and adversely affect any
* existing threads using the old strategy.
* @param strategyName the fully qualified class name of the strategy that should be
* used.
*/
public static void setStrategyName(String strategyName) {
SecurityContextHolder.strategyName = strategyName;
initialize();
}
/**
* Use this {@link SecurityContextHolderStrategy}.
*
* Call either {@link #setStrategyName(String)} or this method, but not both.
*
* This method is not thread safe. Changing the strategy while requests are in-flight
* may cause race conditions.
*
* {@link SecurityContextHolder} maintains a static reference to the provided
* {@link SecurityContextHolderStrategy}. This means that the strategy and its members
* will not be garbage collected until you remove your strategy.
*
* To ensure garbage collection, remember the original strategy like so:
*
* <pre>
* SecurityContextHolderStrategy original = SecurityContextHolder.getContextHolderStrategy();
* SecurityContextHolder.setContextHolderStrategy(myStrategy);
* </pre>
*
* And then when you are ready for {@code myStrategy} to be garbage collected you can
* do:
*
* <pre>
* SecurityContextHolder.setContextHolderStrategy(original);
* </pre>
* @param strategy the {@link SecurityContextHolderStrategy} to use
* @since 5.6
*/
public static void setContextHolderStrategy(SecurityContextHolderStrategy strategy) {
Assert.notNull(strategy, "securityContextHolderStrategy cannot be null");
SecurityContextHolder.strategyName = MODE_PRE_INITIALIZED;
SecurityContextHolder.strategy = strategy;
initialize();
}
/**
* Allows retrieval of the context strategy. See SEC-1188.
* @return the configured strategy for storing the security context.
*/
public static SecurityContextHolderStrategy getContextHolderStrategy() {
return strategy;
}
/**
* Delegates the creation of a new, empty context to the configured strategy.
*/
public static SecurityContext createEmptyContext() {
return strategy.createEmptyContext();
}
}
- MODE THREADLOCAL:这种存放策略是将 SecurityContext 存放在 ThreadLocal 中,⼤家知道 Threadlocal 的特点是在哪个线程中存储就要在哪个线程中读取,这其实⾮常适合 web 应⽤,因为在默认情况下,⼀个请求⽆论经过多少 Filter 到达 Servlet,都是由⼀个线程来处理的。这也是 SecurityContextHolder 的默认存储策略,这种存储策略意味着如果在具体的业务处理代码中,开启了⼦线程,在⼦线程中去获取登录⽤户数据,就会获取不到。
- MODE INHERITABLETHREADLOCAL:这种存储模式适⽤于多线程环境,如果希望在⼦线程中也能够获取到登录⽤户数据,那么可以使⽤这种存储模式。
- MODE GLOBAL:这种存储模式实际上是将数据保存在⼀个静态变量中,在 JavaWeb 开发中,这种模式很少使⽤到。
SecurityContextHolderStrategy
通过 SecurityContextHolder 可以得知, SecurityContextHolderStrategy 接⼝⽤来定义存储策略⽅法
public interface SecurityContextHolderStrategy {
/**
* Clears the current context.
*/
void clearContext();
/**
* Obtains the current context.
*/
SecurityContext getContext();
/**
* Sets the current context.
*/
void setContext(SecurityContext context);
/**
* Creates a new, empty context implementation, for use by
*/
SecurityContext createEmptyContext();
}
从上⾯可以看出每⼀个实现类对应⼀种策略的实现。
获取用户数据
@GetMapping("/hello")
public String hello() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
User user = (User) authentication.getPrincipal();
return user.toString();
}
多线程下获取用户数据
@GetMapping("/hello")
public String hello() {
new Thread(() -> {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
User user = (User) authentication.getPrincipal();
System.out.println(user.toString());
}).start();
return "hello page success";
}
可以看到默认策略,是⽆法在⼦线程中获取⽤户信息,如果需要在⼦线程中获取必须使⽤第⼆种策略,默认策略是通过 System.getProperty 加载的,因此我们可以通过增加 VM Options 参数进⾏修改。
-Dspring.security.strategy=MODE_INHERITABLETHREADLOCAL
⾃定义认证数据源
Servlet Authentication Architecture :: Spring Security
- 发起认证请求,请求中携带⽤户名、密码,该请求会被 UsernamePasswordAuthenticationFilter 拦截
- 在 UsernamePasswordAuthenticationFilter 的 attemptAuthentication ⽅法中将请求中⽤户名和密码,封装为Authentication 对象,并交给 AuthenticationManager 进⾏认证
- 认证成功,将认证信息存储到 SecurityContextHodler 以及调⽤记住我等,并回调 AuthenticationSuccessHandler 处理
- 认证失败,清除 SecurityContextHodler 以及 记住我中信息,回调 AuthenticationFailureHandler 处理
三者关系
从上⾯分析中得知, AuthenticationManager 是认证的核⼼类,但实际上在底层真正认证时还离不开 ProviderManager 以及 AuthenticationProvider 。他们三者关系是样的呢?
- AuthenticationManager 是⼀个认证管理器,它定义了 Spring Security 过滤器要执⾏认证操作。
- ProviderManager AuthenticationManager接⼝的实现类。 Spring Security认证时默认使⽤就是 ProviderManager
- AuthenticationProvider 就是针对不同的身份类型执⾏的具体的身份认证。
AuthenticationManager 与 ProviderManager
ProviderManager 是 AuthenticationManager 的唯⼀实现,也是 Spring Security 默认使⽤实现。从这⾥不难看出默认情况下AuthenticationManager 就是⼀个ProviderManager。
ProviderManager 与 AuthenticationProvider
在 Spring Seourity 中,允许系统同时⽀持多种不同的认证⽅式,例如同时⽀持⽤户名/密码认证、 ReremberMe 认证、⼿机号码动态认证等,⽽不同的认证⽅式对应了不同的 AuthenticationProvider,所以⼀个完整的认证流程可能由多个AuthenticationProvider 来提供。
多个 AuthenticationProvider 将组成⼀个列表,这个列表将由 ProviderManager 代理。换句话说,在ProviderManager 中存在⼀个 AuthenticationProvider 列表,在Provider Manager 中遍历列表中的每⼀个 AuthenticationProvider 去执⾏身份认证,最终得到认证结果。
ProviderManager 本身也可以再配置⼀个 AuthenticationManager 作为 parent,这样当ProviderManager 认证失败之后,就可以进⼊到 parent 中再次进⾏认证。理论上来说, ProviderManager 的 parent 可以是任意类型的AuthenticationManager,但是通常都是由 ProviderManager 来扮演 parent 的⻆⾊,也就是 ProviderManager 是ProviderManager 的 parent。 ProviderManager 本身也可以有多个,多个ProviderManager 共⽤同⼀个 parent。有时,⼀个应⽤程序有受保护资源的逻辑组(例如,所有符合路径模式的⽹络资源,如/api!!*),每个组可以有⾃⼰的专⽤ AuthenticationManager。通常,每个组都是⼀个ProviderManager,它们共享⼀个⽗级。然后,⽗级是⼀种 全局资源,作为所有提供者的后备资源。
Getting Started | Spring Security Architecture
弄清楚认证原理之后我们来看下具体认证时数据源的获取。 默认情况下 AuthenticationProvider 是由 DaoAuthenticationProvider 类来实现认证的,在DaoAuthenticationProvider 认证时⼜通过 UserDetailsService 完成数据源的校验。 他们之间调⽤关系如下:
总结: AuthenticationManager 是认证管理器,在 Spring Security 中有全局 AuthenticationManager,也可以有局部AuthenticationManager。全局的 AuthenticationManager ⽤来对全局认证进⾏处理,局部的 AuthenticationManager ⽤来对某些特殊资源认证处理。当然⽆论是全局认证管理器还是局部认证管理器都是由 ProviderManger 进⾏实现。 每⼀个ProviderManger 中都代理⼀个 AuthenticationProvider 的列表,列表中每⼀个实现代表⼀种身份认证⽅式。认证时底层数据源需要调⽤ UserDetailService 来实现
配置全局 AuthenticationManager
Getting Started | Spring Security Architecture
默认的全局 AuthenticationManager
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
@Autowired
public void initialize(AuthenticationManagerBuilder builder) {
//builder..
}
}
springboot 对 security 进行自动配置时自动在工厂中创建一个全局AuthenticationManager
总结
- 默认自动配置创建全局AuthenticationManager 默认找当前项目中是否存在自定义 UserDetailService 实例 自动将当前项目 UserDetailService 实例设置为数据源
- 默认自动配置创建全局AuthenticationManager 在工厂中使用时直接在代码中注入即可
自定义全局 AuthenticationManager
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
@Override
public void configure(AuthenticationManagerBuilder builder) {
//builder ....
}
}
总结
- 一旦通过 configure 方法自定义 AuthenticationManager实现 就回将工厂中自动配置AuthenticationManager 进行覆盖
- 一旦通过 configure 方法自定义 AuthenticationManager实现 需要在实现中指定认证数据源对象 UserDetaiService 实例
- 一旦通过 configure 方法自定义 AuthenticationManager实现 这种方式创建AuthenticationManager对象工厂内部本地一个 AuthenticationManager 对象 不允许在其他自定义组件中进行注入
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
//1.自定义AuthenticationManager 推荐 并没有在工厂中暴露出来
@Override
public void configure(AuthenticationManagerBuilder builder) throws Exception {
System.out.println("自定义AuthenticationManager: " + builder);
builder.userDetailsService(userDetailsService());
}
//作用: 用来将自定义AuthenticationManager在工厂中进行暴露,可以在任何位置注入
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
自定义内存数据源
@Configuration
public class WebSecurityConfig {
@Bean
public UserDetailsService userDetailsService(){
UserDetails user = User.withUsername("admin").password("{noop}123").roles("ADMIN").build();
return new InMemoryUserDetailsManager(user);
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.mvcMatchers("/index")
.permitAll()
.anyRequest().authenticated()
.and().formLogin()
.successHandler(new LoginSuccessHandler())
.failureHandler(new LoginFailureHandler())
.and().logout().logoutSuccessHandler(new LogoutHandler())
.and().userDetailsService(userDetailsService());
return http.csrf().disable().build();
}
}
自定义数据库数据源
-- 用户表
CREATE TABLE `user`
(
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(32) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
`enabled` tinyint(1) DEFAULT NULL,
`accountNonExpired` tinyint(1) DEFAULT NULL,
`accountNonLocked` tinyint(1) DEFAULT NULL,
`credentialsNonExpired` tinyint(1) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- 角色表
CREATE TABLE `role`
(
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(32) DEFAULT NULL,
`name_zh` varchar(32) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- 用户角色关系表
CREATE TABLE `user_role`
(
`id` int(11) NOT NULL AUTO_INCREMENT,
`uid` int(11) DEFAULT NULL,
`rid` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `uid` (`uid`),
KEY `rid` (`rid`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
-- 插入用户数据
BEGIN;
INSERT INTO `user`
VALUES (1, 'root', '{noop}123', 1, 1, 1, 1);
INSERT INTO `user`
VALUES (2, 'admin', '{noop}123', 1, 1, 1, 1);
INSERT INTO `user`
VALUES (3, 'cheny', '{noop}123', 1, 1, 1, 1);
COMMIT;
-- 插入角色数据
BEGIN;
INSERT INTO `role`
VALUES (1, 'ROLE_product', '商品管理员');
INSERT INTO `role`
VALUES (2, 'ROLE_admin', '系统管理员');
INSERT INTO `role`
VALUES (3, 'ROLE_user', '用户管理员');
COMMIT;
-- 插入用户角色数据
BEGIN;
INSERT INTO `user_role`
VALUES (1, 1, 1);
INSERT INTO `user_role`
VALUES (2, 1, 2);
INSERT INTO `user_role`
VALUES (3, 2, 2);
INSERT INTO `user_role`
VALUES (4, 3, 3);
COMMIT;
项目中引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.29</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.7</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
配置 springboot 配置文件
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/security?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=GMT%2B8&allowMultiQueries=true
username: root
password: root
mybatis:
mapper-locations: mapper/*Mapper.xml
type-aliases-package: com.yang.entity
创建 entity
@Data
public class User implements UserDetails {
private Integer id;
private String username;
private String password;
private Boolean enabled;
private Boolean accountNonExpired;
private Boolean accountNonLocked;
private Boolean credentialsNonExpired;
private List<Role> roles = new ArrayList<>();
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
roles.forEach(role->grantedAuthorities.add(new SimpleGrantedAuthority(role.getName())));
return grantedAuthorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return accountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return accountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return credentialsNonExpired;
}
@Override
public boolean isEnabled() {
return enabled;
}
}
@Data
public class Role {
private Integer id;
private String name;
private String nameZh;
}
创建 UserMapper 接口,编写sql语句
@Mapper
public interface UserMapper {
//根据用户名查询用户
User loadUserByUsername(String username);
//根据用户id查询角色
List<Role> getRolesByUid(Integer uid);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yang.mapper.UserMapper">
<!--查询单个-->
<select id="loadUserByUsername" resultType="com.yang.entity.User">
select id,
username,
password,
enabled,
accountNonExpired,
accountNonLocked,
credentialsNonExpired
from user
where username = #{username}
</select>
<!--查询指定行数据-->
<select id="getRolesByUid" resultType="com.yang.entity.Role">
select r.id,
r.name,
r.name_zh nameZh
from role r,
user_role ur
where r.id = ur.rid
and ur.uid = #{uid}
</select>
</mapper>
创建 service
public interface UserService {
UserDetails loadUserByUsername(String username);
}
@Service
public class UserServiceImpl implements UserService {
private final UserMapper userMapper;
@Autowired
public UserServiceImpl(UserMapper userMapper) {
this.userMapper = userMapper;
}
@Override
public UserDetails loadUserByUsername(String username) {
User user = userMapper.loadUserByUsername(username);
if(ObjectUtils.isEmpty(user)){
throw new RuntimeException("用户不存在");
}
user.setRoles(userMapper.getRolesByUid(user.getId()));
return user;
}
}
创建 UserDetailsService
@Component
public class UserDetailService implements UserDetailsService {
private final UserService userService;
public UserDetailService(UserService userService) {
this.userService = userService;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userService.loadUserByUsername(username);
}
}
配置 authenticationManager 使用自定义UserDetailService
@Configuration
public class SecurityWebConfig {
private final UserDetailService userDetailService;
public SecurityWebConfig(UserDetailService userDetailService) {
this.userDetailService = userDetailService;
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.mvcMatchers("/index")
.permitAll()
.anyRequest().authenticated()
.and().formLogin()
.successHandler(new LoginSuccessHandler())
.failureHandler(new LoginFailureHandler())
.and().logout().logoutSuccessHandler(new LogoutHandler()) // 注销登入处理器
.and().userDetailsService(userDetailService); // 自定义数据源
return http.csrf().disable().build();
}
}
添加验证码
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
生成验证码
@Configuration
public class KaptchaConfig {
@Bean
public Producer kaptcha() {
Properties properties = new Properties();
properties.setProperty("kaptcha.image.width", "150");
properties.setProperty("kaptcha.image.height", "50");
properties.setProperty("kaptcha.textproducer.char.string", "0123456789");
properties.setProperty("kaptcha.textproducer.char.length", "4");
Config config = new Config(properties);
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
@RestController
public class KaptchaController {
private final Producer producer;
public KaptchaController(Producer producer) {
this.producer = producer;
}
@GetMapping("/vc.png")
public String getVerifyCode(HttpSession session) throws IOException {
//1.生成验证码
String code = producer.createText();
session.setAttribute("kaptcha", code);//可以更换成 redis 实现
BufferedImage bi = producer.createImage(code);
//2.写入内存
FastByteArrayOutputStream fos = new FastByteArrayOutputStream();
ImageIO.write(bi, "png", fos);
//3.生成 base64
return Base64.encodeBase64String(fos.toByteArray());
}
}
定义验证码异常类
public class KaptchaNotMatchException extends AuthenticationException {
public KaptchaNotMatchException(String msg) {
super(msg);
}
public KaptchaNotMatchException(String msg, Throwable cause) {
super(msg, cause);
}
}
在自定义LoginKaptchaFilter中加入验证码验证
/**
* @Author: chenyang
* @DateTime: 2023/2/27 10:14
* @Description: 自定义过滤器
*/
public class LoginKaptchaFilter extends UsernamePasswordAuthenticationFilter {
public static final String FORM_CAPTCHA_KEY = "captcha";
private String kaptchaParameter = FORM_CAPTCHA_KEY;
public String getKaptchaParameter() {
return kaptchaParameter;
}
public void setKaptchaParameter(String kaptchaParameter) {
this.kaptchaParameter = kaptchaParameter;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
try {
//1.获取请求数据
Map<String, String> userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
String kaptcha = userInfo.get(getKaptchaParameter());//用来获取数据中验证码
String username = userInfo.get(getUsernameParameter());//用来接收用户名
String password = userInfo.get(getPasswordParameter());//用来接收密码
//2.获取 session 中验证码
String sessionVerifyCode = (String) request.getSession().getAttribute(FORM_CAPTCHA_KEY);
if (!ObjectUtils.isEmpty(kaptcha) && !ObjectUtils.isEmpty(sessionVerifyCode) &&
kaptcha.equalsIgnoreCase(sessionVerifyCode)) {
//3.获取用户名 和密码认证
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
} catch (IOException e) {
e.printStackTrace();
}
throw new KaptchaNotMatchException("验证码不匹配!");
}
}
配置
@Configuration
public class WebSecurityConfig {
private final UserDetailService userDetailService;
public WebSecurityConfig(UserDetailService userDetailService) {
this.userDetailService = userDetailService;
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public LoginKaptchaFilter loginKaptchaFilter(AuthenticationManager authenticationManager) {
LoginKaptchaFilter filter = new LoginKaptchaFilter();
//1.认证 url
filter.setFilterProcessesUrl("/doLogin");
//2.认证 接收参数
filter.setUsernameParameter("username");
filter.setPasswordParameter("pwd");
filter.setKaptchaParameter("kaptcha");
//3.指定认证管理器
filter.setAuthenticationManager(authenticationManager);
// 4.指定成功/失败时处理
filter.setAuthenticationSuccessHandler(new LoginSuccessHandler());
filter.setAuthenticationFailureHandler(new LoginFailureHandler());
return filter;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.mvcMatchers("/index", "/vc.png")
.permitAll()
.anyRequest().authenticated()
.and().formLogin()
.and().logout().logoutSuccessHandler(new LogoutHandler()) // 注销登入处理器
.and().exceptionHandling().authenticationEntryPoint(new UnAuthenticationHandler()) // 未认证处理器
.and().userDetailsService(userDetailService) // 自定义数据源
.addFilterBefore(loginKaptchaFilter(http.getSharedObject(AuthenticationManager.class)), UsernamePasswordAuthenticationFilter.class); // 自定义过滤器
return http.csrf().disable().build();
}
}
自定义认证异常处理类
/**
* @Author: chenyang
* @DateTime: 2023/2/27 11:27
* @Description: 未认证时请求处理器
*/
public class UnAuthenticationHandler implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().println("必须认证之后才能访问!");
}
}
spring cloud security 中的 AuthenticationEntryPoint 设置与 AccessDeniedException 捕获过程 - 掘金
SpringSecurity系列 之 AuthenticationEntryPoint接口及其实现类的用法_oauth2authenticationentrypoint_姠惢荇者的博客-CSDN博客
测试验证
调用接口获取图片的Base64 编码,再将编码转换成图片
登入
调用获取验证码接口时会自动保存session