侧边栏壁纸
博主头像
LYMTICS

海纳百川,有容乃大

  • 累计撰写 45 篇文章
  • 累计创建 37 个标签
  • 累计收到 19 条评论

目 录CONTENT

文章目录

SpringSecurity的自定义

LYMTICS
2022-03-16 / 2 评论 / 1 点赞 / 174 阅读 / 24,275 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2022-03-20,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

SpringSecurity的自定义

学习资源:

自定义认证处理

资源权限规则

按照上面说的,可以通过实现 WebSecurityConfigurerAdapter 来覆盖默认配置信息:

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.authorizeRequests()
				.mvcMatchers("/index").permitAll()	// 放行资源放在任何前面
				.anyRequest().authenticated()
				.and()
				.formLogin();
	}
}

如上即可放行 index 资源

自定义登录页面

引入 thymeleaf

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
spring:
  thymeleaf:
    cache: false

resources/templates/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>用户登录</h1>
    <form action="">
        用户名:<input type="text" name="uname"><br>
        密码:<input type="text" name="passwd"><br>
    </form>
</body>
</html>

LoginController

@Controller
public class LoginController {

	@RequestMapping("/login.html")
	public String login() {
		return "login";
	}
}

Config/WebSecurityConfigurer : 放行该登陆页面,并设置默认登录页面

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.authorizeRequests()
				.mvcMatchers("/login.html").permitAll()
				.mvcMatchers("/index").permitAll()	// 放行资源放在任何前面
				.anyRequest().authenticated()
				.and()
				.formLogin()
				.loginPage("/login.html"); // 设置默认登录页面
	}
}

此时只是前端方面的把页面换到了我们自定义的页面,还不能进行认证功能。

我们看一下默认的配置是如何做到的:路径:formLogin() -> new FormLoginConfigurer<>() -> new UsernamePasswordAuthenticationFilter()

pximage

  1. 方法必须是 POST
  2. 用户名name必须是 username
  3. 密码的name必须是 password
  4. 【还有一点没有体现的是】这里还有一个防止CSRF跨站请求的参数

如果我们想利用默认的验证逻辑,可以这么做:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>用户登录</h1>
    <form th:action="@{/doLogin}" method="post">
        用户名:<input type="text" name="username"><br>
        密码:<input type="text" name="password"><br>
        <input type="submit" value="登录">
    </form>
</body>
</html>

注意上述的请求方法和name属性都是按照上面说的来的

配置信息:

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.authorizeRequests()
				.mvcMatchers("/login.html").permitAll()
				.mvcMatchers("/index").permitAll()	// 放行资源放在任何前面
				.anyRequest().authenticated()
				.and()
				.formLogin()
				.loginPage("/login.html")
				.loginProcessingUrl("/doLogin") // 指定处理登录请求的 URL
				.and()
				.csrf().disable();
	}
}

有一些配置:

pximage

自定义登录成功处理

有时候页面跳转并不能满足我们,特别是在前后端分离开发中就不需要成功之后跳转页面。只需要给前端返回一个JSON通知登录成功还是失败与否。这个时候可以通过自定义 AuthenticationSuqccessHandler 实现。

public class MyHandler implements AuthenticationSuccessHandler {

	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
		Map<String, Object> result = new HashMap<>();
		result.put("msg", "登录成功");
		result.put("status", 200);
		result.put("authentication", authentication);
		response.setContentType("application/json;charset=UTF-8");
		String s = new ObjectMapper().writeValueAsString(result);
		response.getWriter().println(s);
	}
}
http.authorizeRequests()
    .mvcMatchers("/login.html").permitAll()
    .mvcMatchers("/index").permitAll()	// 放行资源放在任何前面
    .anyRequest().authenticated()
    .and()
    .formLogin()
    .loginPage("/login.html")
    .loginProcessingUrl("/doLogin") // 指定处理登录请求的 URL
    .successHandler(new MyHandler()) // 指定Handler
    .and()
    .csrf().disable();

自定义失败处理

这个和前面这一部分比较像,所以就简单写一下:

// 代码片段

// forward 保存在Request中
.failureForwardUrl("/login.html")
// 默认 redirect 保存在Session中
.failureUrl("/login.html") 
// 自定义处理器 适合前后端分离
.failureHandler(new MyAuthenticationFailureHandler())

注销登录

默认访问 /logout 即可注销

// 代码片段

.logout()
.logoutUrl("/out") // 指定注销登录 url
.invalidateHttpSession(true) // 默认 绘画失效
.clearAuthentication(true) // 默认 清除认证标记
.logoutSuccessUrl("/login.html") // 注销成功后跳转的页面

默认请求方式是 POST

需要修改:

.logout()
.logoutRequestMatcher(new OrRequestMatcher(
	new AntPathRequestMatcher("/logout1", "GET"),
    new AntPathRequestMatcher("/logout2", "POST")
))

如果前后端分离,或者业务原因,可以改为:

.logoutSuccessHandler(new MyLogoutSuccesssHandle())

具体类似上面,就不具体写了。

获取用户信息

有两种情况:

  1. 传统开发模式,代码和页面中如何获取
  2. 前后端分离,如何获取

用户认证成功后存在哪里?

Spring Security 会将登录用户数据保存在 Session 中。但是为了使用方便,其有所改进,其中最重要的一个变化就是线程绑定。当用户登录成功后,SpringSecurity 会将登陆成功的用户信息保存到 SecurityContextHolder 中。

SecurityContextHolder 中的数据保存默认是通过 ThreadLocal 来实现的,使用ThreadLocal 创建的变量只能被当前线程访问,不能被其他线程访问和修改,也就是用户数据和请求线程绑定在一起。当登录请求处理完毕后,Spring Security 会将 SecurityContextHolder 中的数据拿出来保存到 Session 中, 同时将 SecurityContextHolder 中的数据清空。以后每当有请求到来时, Spring Security 就会先从 Session 中取出用户登录数据, 保存到 SecurityContextHolder 中的数据拿出来保存到 Session 中, 然后将 SecurityContextHolder 中的数据清空。

pximage

SecurityContextHolder

pximage

策略模式,根据具体策略进行相应的处理

  1. MODE THREADLOCAL︰这种存放策略是将SecurityContext存放在ThreadLocal中,大家知道Threadlocal的特点是在哪个线程中存储就要在哪个线程中读取,这其实非常适合web应用,因为在默认情况下,一个请求无论经过多少Filter到达 Servlet,都是由一个线程来处理的。这也是SecurityContextHolder的默认存储策略,这种存储策略意味着如果在具体的业务处理代码中,开启了子线程,在子线程中去获取登录用户数据,就会获取不到
  2. MODE INHERITABLETHREADLOCAL︰这种存储模式适用于多线程环境,如果希望在子线程中也能够获取到登录用户数据,那么可以使用这种存储模式。
  3. MODE GLOBAL︰这种存储模式实际上是将数据保存在一个静态变量中,在JavaWeb开发中,这种模式很少使用到。

注意到如下代码:

private static String strategyName = System.getProperty("spring.security.strategy");

所以要想自己改变策略,需要在运行 Java 程序时带上参数:

-Dspring.security.strategy=MODE_INHERITABLETHREADLOCAL

SecurityHolderStrategy

存储具体的策略方法,如上图所示。

在代码中获取信息

编辑如下代码:

@GetMapping("/auth")
public String getAuth() {
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    System.out.println(authentication.getPrincipal());
    System.out.println(authentication.getAuthorities());
    return "Auth";
}

给用户设置角色:

pximage

如果是前后端的方式,则可以将此数据返回

在页面中获得用户信息

这里就直接粘贴一下视频中的做法了:

pximage

自定义认证数据源

认证流程分析

前面我们在看到 SpringBootWebSecurityConfiguration 时,有如下代码:

@Configuration(proxyBeanMethods = false)
@ConditionalOnDefaultWebSecurity
@ConditionalOnWebApplication(type = Type.SERVLET)
class SpringBootWebSecurityConfiguration {
    @Bean
    @Order(SecurityProperties.BASIC_AUTH_ORDER)
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) 
    throws Exception {
    // 要求所有请求都要认证
    http.authorizeRequests().anyRequest().authenticated()
    .and().formLogin().and().httpBasic();
        return http.build();
    }
}

其中的 formLogin 负责对请求进行验证。

我们点进去,发现其函数如下:

public FormLoginConfigurer<HttpSecurity> formLogin() throws Exception {
    return getOrApply(new FormLoginConfigurer<>());
}

我们继续点入这个 FormLoginConfigurer 类:

public FormLoginConfigurer() {
    super(new UsernamePasswordAuthenticationFilter(), null);
    usernameParameter("username");
    passwordParameter("password");
}

看到这里创建了一个 UsernamePasswordAuthenticationFilter ,看名字,这应该是一个 Filter 。

Spring Security 的 Filter 不是对默认 Servlet Filter 的实现,而是其自定的一个 Filter 接口: AbstractAuthenticationProcessingFilter

实现 Filter 时,我们关注他的 doFilter 方法,而这里我们关注的是他的 attemptAuthentication 方法。

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
    if (this.postOnly && !request.getMethod().equals("POST")) {
        throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
    } else {
        String username = this.obtainUsername(request);
        username = username != null ? username : "";
        username = username.trim();
        String password = this.obtainPassword(request);
        password = password != null ? password : "";
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
        this.setDetails(request, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }
}

这一段的前面分别判断了请求的格式是不是POST,以及对请求参数的获取和封装,最后我们注意到它调用了 this.getAuthenticationManager().authenticate(authRequest); 方法。这是一个关键。

那我们知道,这个 this.getAuthenticationManager() 获得的就是实现了 AuthenticationManager 的对象,而这里其实默认就是 ProviderManager 的 authenticate 方法:

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		Class<? extends Authentication> toTest = authentication.getClass();
		AuthenticationException lastException = null;
		AuthenticationException parentException = null;
		Authentication result = null;
		Authentication parentResult = null;
		int currentPosition = 0;
		int size = this.providers.size();
		for (AuthenticationProvider provider : getProviders()) {
			if (!provider.supports(toTest)) {
				continue;
			}
			if (logger.isTraceEnabled()) {
				logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
						provider.getClass().getSimpleName(), ++currentPosition, size));
			}
			try {
				result = provider.authenticate(authentication);
				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException | InternalAuthenticationServiceException ex) {
				prepareException(ex, authentication);
				// SEC-546: Avoid polling additional providers if auth failure is due to
				// invalid account status
				throw ex;
			}
			catch (AuthenticationException ex) {
				lastException = ex;
			}
		}
		if (result == null && this.parent != null) {
			// Allow the parent to try.
			try {
				parentResult = this.parent.authenticate(authentication);
				result = parentResult;
			}
			catch (ProviderNotFoundException ex) {
				// ignore as we will throw below if no other exception occurred prior to
				// calling parent and the parent
				// may throw ProviderNotFound even though a provider in the child already
				// handled the request
			}
			catch (AuthenticationException ex) {
				parentException = ex;
				lastException = ex;
			}
		}
		if (result != null) {
			if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
				// Authentication is complete. Remove credentials and other secret data
				// from authentication
				((CredentialsContainer) result).eraseCredentials();
			}
			// If the parent AuthenticationManager was attempted and successful then it
			// will publish an AuthenticationSuccessEvent
			// This check prevents a duplicate AuthenticationSuccessEvent if the parent
			// AuthenticationManager already published it
			if (parentResult == null) {
				this.eventPublisher.publishAuthenticationSuccess(result);
			}

			return result;
		}

		// Parent was null, or didn't authenticate (or throw an exception).
		if (lastException == null) {
			lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
					new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
		}
		// If the parent AuthenticationManager was attempted and failed then it will
		// publish an AbstractAuthenticationFailureEvent
		// This check prevents a duplicate AbstractAuthenticationFailureEvent if the
		// parent AuthenticationManager already published it
		if (parentException == null) {
			prepareException(lastException, authentication);
		}
		throw lastException;
	}

这段代码太长了,具体就是验证工作,不过,有这么一行代码:

parentResult = this.parent.authenticate(authentication);

往前看一下,他的 parent 是谁:

private AuthenticationManager parent;

也是一个 AuthenticationManager 接口的实现类,那不又是 ProviderManager 吗(注意之前提到了,这个类比较常用和重要)

这就形成了一种 责任链模式 的感觉。【自我感觉】

这样的话有一些公共的认证操作或许可以在同一个 ProviderManager 中去实现?【自我感觉】

pximage

总结

  • AuthenticationManager 是一个认证管理器,定义了SpringSecurity过滤器要执行的认证操作
  • ProviderManager 是上面接口的实现类,默认用的就是这个
  • AuthenticationProvider 是针对不同身份类型执行的具体的身份认证,一个ProviderManager有一个List列表来保存多个 AuthenticationProvider 实例

流程观察

我们可以以 Debug 模式运行一下,结果如下:

  1. 第一次到 ProviderManager 时,此时其仅有的 provider 为一个匿名认证类,所以在 if 判断中条件成立,跳过了这一轮 for 循环(由于只有一个,其实直接跳过了 for 循环)

    pximage

  2. 跳过循环后,继续判断其有没有parent,这里一看,发现它确实有,所以就继续让这个父亲执行:

    pximage

  3. 此时发现父类有另一个类的 provider,并且满足条件,于是进入,并调用这个 provider 进行认证相关的操作:

    pximage

  4. 这里需要注意的就比较多了:左侧箭头程序通过调用 retrieveUser 方法获得用户的信息,注意到这是一个抽象的方法,具体实现在其实现类中(看右侧方框)。
    另外,通过这个方法获得的对象 user 的属性如下面所示;这里密码也显示出来了, {noop} 表示这是明文加密的。
    为什么获取用户信息和密码验证是分开的呢?主要是加密的方法是不确定的,所以调用不同的方法进行解密或着说叫验证

    pximage

  5. 最后,再看一眼刚刚那个 retrieveUser 的方法,因为这里保存了如何获取数据源的重要信息!可以看到,这里 getUserDetailsService 返回了 UserDetailsService 的实现类,这里就是 InMemoryUserDetailsManager 这个类!这就和我们前面说的连上了!

    pximage

我们通过其执行流程来对这一部分的内容有了一个更深的了解,另外,如果我们要用自己的数据源做登录验证,那么完成 UserDetailsService 的实现类是不是就可以了呢?

配置全局AuthenticationManager

通过配置这个类,来实现一些自定义的功能。

Sping 提供了两种方式来做这件事情:

方法一:【推荐】

// SpringBoot 对 security 默认配置中 在工厂中默认创建 AuthenticationManager
// 我们可以获得这个,然后对其进行配置
@Autowired
public void initialize(AuthenticationManagerBuilder builder) throws Exception {
    InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
    userDetailsManager.createUser(User.withUsername("aaa").password("{noop}123").roles("admin").build());
    builder.userDetailsService(userDetailsManager);
}

此时原来系统默认创建的那个类由于不满足 @ConditionalOnMissingBeans 条件而不创建,所以我们只能通过自己在代码中指定的用户名和密码进行登录

对!其实我们也可以直接创建一个 Bean:

@Bean
public UserDetailsService userDetailsService() {
    InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
    userDetailsManager.createUser(User.withUsername("aaa").password("{noop}123").roles("admin").build());
    return userDetailsManager;
}

或者完全覆盖:

// 方法二:自定义 AuthenticationManager 覆盖父类
@Override
public void configure(AuthenticationManagerBuilder builder) {
	// XXX
}

但是这样的话:

  1. 他可没有自动配置的功能,所以你还得手动给他配置一个 UserDetailsService 类,不然就无法正常工作了!

  2. 此时这个类是一个本地的,也就是说你无法在其他的地方通过自动注入的方式获得,除非你重写了authenticationManagerBean() 方法:

    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    

自定义数据源实战

设计数据库

user 表的设置参考 User 类的字段:

pximage

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, 'song', '{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, 4);
  INSERT INTO `user_role`
  VALUES (4, 3, 3);
COMMIT;

DAO层

这里可以引入 MyBatis 之类的,下面只列出部分片段,具体怎么做就不在本文的讨论范围了

dao.UserDao

@Mapper
public interface UserDao {
    User loadUserByUsername(String username);
    List<Role> getRolesByUid(Integer uid);
}

service.MyUserDetailService

@Component
public class MyUserDetailService implements UserDetailsService {
    private final UserDao userDao;
    
    @Autowired
    public MyUserDetailService(UserDao userDao) {
        this.userDao = userDao;
    }
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userDao.loadUserByUsername(username);
        if (ObjectUtils.isEmpty(user)) {
            throws new UsernameNotFoundException("用户名不正确");
            List<Role> roles = userDao.getRolesByUid(user.getId());
            user.setRoles(roles);
            return user;
        }
    }
}

配置

配置 authenticationManager

@Autowired
private MyUserDetailService myUserDetailService;

@Override
public void configure(AuthenticationManagerBuilder builder) throws Exception {
    builder.userDetailsService(myUserDetailService);
}

实战

传统实战

暂略

前后端分离实战

传统开发,一般会跳转到某一个页面,比如成功后到哪个,失败后到哪个等等。这不太适合于前后端分离的项目,在前后端分离的项目中,应该如果没有登陆,应该返回一个 401 ,如果成功或失败,应该返回相应的 JSON 数据。

怎么做呢?我们看到 UsernamePasswordAuthenticationFilter 中的 attemptAuthentication 方法解决了如何获取请求参数的问题:

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
    if (this.postOnly && !request.getMethod().equals("POST")) {
        throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
    } else {
        String username = this.obtainUsername(request);
        username = username != null ? username : "";
        username = username.trim();
        String password = this.obtainPassword(request);
        password = password != null ? password : "";
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
        this.setDetails(request, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }
}

这里判断了请求方式,以及获取请求中的 username 和 password,我们要覆盖这个 Filter , 改为从请求体的 JSON 中获取相关的数据。

我们自定义一个 LoginFilter

public class LoginFilter extends UsernamePasswordAuthenticationFilter {
	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
		System.out.println("==========================");
		if (!request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
		}
		if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {
			Map<String, String> userInfo = null;
			try {
				userInfo =  new ObjectMapper().readValue(request.getInputStream(), Map.class);
			} catch (IOException e) {
				e.printStackTrace();
			}
			String username = userInfo.get(getUsernameParameter());
			String password = userInfo.get(getPasswordParameter());
			UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
			setDetails(request, authRequest);
			return this.getAuthenticationManager().authenticate(authRequest);
		}
		return super.attemptAuthentication(request, response);
	}
}

然后再自定义配置:

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {

    // 这里是上面部分提到的,自定义的数据源
    // 这里用 InMemory 的方式以方便一些
	@Bean
	public UserDetailsService userDetailsService() {
		InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
		inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("admin").build());
		return inMemoryUserDetailsManager;
	}

	// 自定义 AuthenticationManager 覆盖父类
	@Override
	public void configure(AuthenticationManagerBuilder builder) throws Exception {
		builder.userDetailsService(userDetailsService());
	}

	// 前面提到过,用configure方式覆盖需要自己把他导出
	@Override
	@Bean
	public AuthenticationManager authenticationManagerBean() throws Exception {
		return super.authenticationManagerBean();
	}

	// 创建实例并初始化
	@Bean
	public LoginFilter loginFilter() throws Exception {
		LoginFilter loginFilter = new LoginFilter();
		loginFilter.setFilterProcessesUrl("/doLogin");
		loginFilter.setUsernameParameter("uname");
		loginFilter.setPasswordParameter("passwd");
		loginFilter.setAuthenticationManager(authenticationManagerBean());
		loginFilter.setAuthenticationSuccessHandler((req, resp, authentication)->{
			HashMap<String, Object> result = new HashMap<>();
			result.put("msg", "登录成功");
			result.put("用户信息", authentication.getPrincipal());
			resp.setContentType("application/json;charset=UTF-8");
			resp.setStatus(HttpStatus.OK.value());
			String s = new ObjectMapper().writeValueAsString(result);
			resp.getWriter().println(s);
		});
		loginFilter.setAuthenticationFailureHandler((req, resp, ex) -> {
			HashMap<String, Object> result = new HashMap<>();
			result.put("msg", "登录失败" + ex.getMessage());
			resp.setContentType("application/json;charset=UTF-8");
			resp.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
			String s = new ObjectMapper().writeValueAsString(result);
			resp.getWriter().println(s);
		});
		return loginFilter;
	}

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.authorizeRequests()
				.anyRequest().authenticated()
				.and()
				.formLogin()
				.and()
				.exceptionHandling()
            	.authenticationEntryPoint((req, resp, ex) -> {
                    resp.setContextrType(MediaType.APPLICATION_JSON_VALUE);
                    resp.setStatus(HttpStatus.UNAUTHORIZED.value());
                    resp.getWriter().println("请认证之后再去处理");
                })
				.and()
				.logout()
				.logoutUrl("/logout")
				.logoutSuccessHandler((req, resp, authentication) -> {
					HashMap<String, Object> result = new HashMap<>();
					result.put("msg", "注销成功");
					result.put("用户信息", authentication.getPrincipal());
					resp.setContentType("application/json;charset=UTF-8");
					resp.setStatus(HttpStatus.OK.value());
					String s = new ObjectMapper().writeValueAsString(result);
					resp.getWriter().println(s);
				})
				.and().csrf().disable();
		http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
	}
}

要注意最后几行:

http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
  • at:用来某个 filter 替换过滤器链中的哪个 filter
  • before:放在过滤器链中哪个filter之前
  • after:放在过滤器链中哪个filter之后

pximage

验证码功能

传统模式

流程:

  1. 引入相关库,配置生成验证码的属性

    <dependency>
        <groupId>com.github.penggle</groupId>
        <artifactId>kaptcha</artifactId>
        <version>2.3.2</version>
    </dependency>
    

    config.KaptchaConfig

    @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;
    	}
    }
    
  2. 自定义Filter,保存验证码文本到 Session 中,生成图片并返回
    security.filter.KaptchaFilter

    public class KaptchaFilter extends UsernamePasswordAuthenticationFilter {
    	private static final String FORM_KAPTCHA_KEY = "kaptcha";
    	@Getter
    	@Setter
    	private String kaptchaParameter = FORM_KAPTCHA_KEY;
    	@Override
    	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
    		if (!request.getMethod().equals("POST")) {
    			throw new AuthenticationServiceException("MethodsError");
    		}
    
    		String verifyCode = (String) request.getSession().getAttribute("kaptcha");
    
    		if (!ObjectUtils.isEmpty(verifyCode) && !ObjectUtils.isEmpty(verifyCode) && verifyCode.equalsIgnoreCase(request.getParameter(getKaptchaParameter()))) {
    			return super.attemptAuthentication(request, response);
    		}
    		throw new KaptchaNotMatchException("验证码不匹配");
    	}
    }
    

    自定义一个验证码错误的异常:

    security.Exception.KaptchaNotMatchException

    public class KaptchaNotMatchException extends AuthenticationException {
    	public KaptchaNotMatchException(String msg, Throwable cause) {
    		super(msg, cause);
    	}
    
    	public KaptchaNotMatchException(String msg) {
    		super(msg);
    	}
    }
    
  3. 将我们自定义的 Filter 替换掉 原来的:

    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    	@Bean
    	public UserDetailsService userDetailsService() {
    		InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
    		inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("admin").build());
    		return inMemoryUserDetailsManager;
    	}
    
    
    	@Override
    	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    		auth.userDetailsService(userDetailsService());
    	}
    
    
    	@Override
    	@Bean
    	public AuthenticationManager authenticationManagerBean() throws Exception {
    		return super.authenticationManagerBean();
    	}
    
    	@Bean
    	public KaptchaFilter kaptchaFilter() throws Exception {
    		KaptchaFilter kaptchaFilter = new KaptchaFilter();
    		kaptchaFilter.setFilterProcessesUrl("/doLogin");
    		kaptchaFilter.setPasswordParameter("passwd");
    		kaptchaFilter.setUsernameParameter("uname");
    		kaptchaFilter.setKaptchaParameter("kaptcha");
    		kaptchaFilter.setAuthenticationManager(authenticationManagerBean());
    		kaptchaFilter.setAuthenticationSuccessHandler((req, resp, auth) -> {
    			System.out.println(auth);
    			resp.sendRedirect("/index.html");
    		});
    		kaptchaFilter.setAuthenticationFailureHandler((req, resp, ex) -> {
    			System.out.println(ex);
    			resp.sendRedirect("/login.html");
    		});
    		return kaptchaFilter;
    	}
    
    	@Override
    	protected void configure(HttpSecurity http) throws Exception {
    		http.authorizeRequests()
    				.mvcMatchers("/login.html").permitAll()
    				.mvcMatchers("/vc.jpg").permitAll()
    				.anyRequest().authenticated()
    				.and()
    				.formLogin()
    				.and()
    				.logout()
    				.logoutUrl("/logout")
    				.and()
    				.csrf().disable();
    		http.addFilterAt(kaptchaFilter(), UsernamePasswordAuthenticationFilter.class);
    	}
    }
    

前后端模式

不能直接返回一个图片,应该返回一个 base64 格式的验证码

pximage

自定义加密

自动登录

参考文章:

小结

这是第一次学习 SpringSecurity 这个框架,尽管这个视频教的很好,涉及到了许多源码的部分,但是还是感觉有一点没有掌握整体框架的感觉。比如:

  1. 为什么一会儿Handler配置在 HttpSecurity 上,一会儿又配置在 Filter
  2. 我发现不同的人在用这个的时候风格还是很不一样的,哪种方式更好一些,或者没有影响
  3. 虽然了解了各个小的组件,但是从整体来看这些组件是如何相互协作的
  4. CSRF 具体怎么配置,前面一直都是把他关闭了的
  5. 这里似乎只学了认证,没有学鉴权

由于实用的观念和时间的关系,这篇文章先告一段落,之后等需要学习的时候再进行补充!

1

评论区