SpringBoot进阶教程(八十一)Spring Security自定义认证
在上一篇博文《SpringBoot进阶教程(八十)Spring Security》中,已经介绍了在Spring Security中如何基于formLogin认证、基于HttpBasic认证和自定义用户名和密码。这篇文章,我们将介绍自定义登录界面的登录验证方式。
v定义认证过程
自定义认证的过程会用到Spring Security提供的UserDetail接口。源码如下:

自定义认证的过程还会用到Spring Security提供的UserDetailService接口,接口只有一个抽象方法loadUserByUsername,loadUserByUsername方法返回一个UserDetail对象,包含一些用于描述用户信息的方法,源码如下:

在项目中可以自定义UserDetails接口的实现类,直接使用Spring Security提供的UserDetails接口实现类org.springframework.security.core.userdetails.User也是可以的。
/**
* @Author chen bo
* @Date 2023/12
* @Des
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserLogin implements UserDetails {
private String username;
private String password; /**
* 获取用户包含的权限,返回权限集合,权限是一个继承了GrantedAuthority的对象;
* @return
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities(){
return AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
} /**
* 判断账户是否未过期,未过期返回true反之返回false
* @return
*/
@Override
public boolean isAccountNonExpired(){
return true;
} /**
* 判断账户是否未锁定
* @return
*/
@Override
public boolean isAccountNonLocked(){
return true;
} /**
* 判断用户凭证是否没过期,即密码是否未过期
* @return
*/
@Override
public boolean isCredentialsNonExpired(){
return true;
} /**
* 判断用户是否可用
* @return
*/
@Override
public boolean isEnabled(){
return true;
}
}
我们先创建一个service层的方法,用户模拟获取获取。实际中一般从数据库或者redis中获取。这里为了简化,我们直接将用户信息写在内存中。
创建模拟DB的PO实体UserPo
/**
* @Author chen bo
* @Date 2023/12
* @Des
*/
@Data
public class UserPo {
private String userName;
private String pwd;
}
创建获取用户数据的service接口
/**
* @Author chen bo
* @Date 2023/12
* @Des
*/
public interface UserService {
/**
* 根据用户名获取用户信息
* @param userName
* @return
*/
UserPo getUserByUserName(String userName);
}
创建获取用户数据service接口的实现类。
/**
* @Author chen bo
* @Date 2023/12
* @Des
*/
@Service
@Slf4j
public class UserServiceImpl implements UserService {
@Override
public UserPo getUserByUserName(String userName){
List<UserPo> userPoList = userPoList();
if(CollectionUtils.isEmpty(userPoList)){
return null;
} return userPoList.stream().filter(item -> userName.equals(item.getUserName())).findAny().orElse(null);
} /**
* 正常这一步应该是在DB或者redis中查询的,这里为了简化demo流程,直接在内存中写入固定用户集合
* @return
*/
private List<UserPo> userPoList(){
List<UserPo> userPoList = new ArrayList<>();
UserPo userPo = new UserPo();
userPo.setUserName("zhangsan");
userPo.setPwd("zs123456");
userPoList.add(userPo);
userPo = new UserPo();
userPo.setUserName("lisi");
userPo.setPwd("ls123456");
userPoList.add(userPo);
userPo = new UserPo();
userPo.setUserName("wangwu");
userPo.setPwd("ww123456");
userPoList.add(userPo);
return userPoList;
}
}
下面我们来开始实现UserDetailService接口的loadUserByUsername方法。首先创建一个UserLogin(UserDetails接口的实现类)对象。接着创建UserDetailServiceImpl实现UserDetailService。
/**
* @Author chen bo
* @Date 2023/12
* @Des
*/
@Configuration
@Slf4j
public class UserDetailServiceImpl implements UserDetailsService { @Autowired
private PasswordEncoder passwordEncoder; @Autowired
UserService userService; @Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
UserPo userPo = userService.getUserByUserName(userName);
if(userPo == null){
throw new RuntimeException("用户名或密码错误");
} UserLogin user = new UserLogin();
user.setUsername(userPo.getUserName());
user.setPassword(passwordEncoder.encode(userPo.getPwd())); log.info("password : " + user.getPassword());
return user;
}
}
由于权限参数不能为空,所以这里先使用AuthorityUtils.commaSeparatedStringToAuthorityList方法模拟一个admin的权限,该方法可以将逗号分隔的字符串转换为权限集合。
此外我们还注入了PasswordEncoder对象,该对象用于密码加密,注入前需要手动配置。我们在BrowserSecurityConfig中配置它:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// ......
}
PasswordEncoder是一个密码加密接口,而BCryptPasswordEncoder是Spring Security提供的一个实现方法,我们也可以自己实现PasswordEncoder。不过Spring Security实现的BCryptPasswordEncoder已经足够强大,它对相同的密码进行加密后可以生成不同的结果。
这时候重启项目,访问http://localhost:9090/login,便可以使用user以及123456作为密码登录系统。
注意:BCryptPasswordEncoder对相同的密码生成的结果每次都是不一样的
v重写form登录页
默认的登录页面过于简陋,我们可以自己定义一个登录页面。为了方便起见,我们直接在src/main/resources/resources目录下定义一个login.html(不需要Controller跳转)。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
<link rel="stylesheet" href="css/login.css" type="text/css">
</head>
<body>
<form class="login-page" action="/login" method="post">
<div class="form">
<h3>请登录</h3>
<input type="text" placeholder="请输入用户名" name="username" required="required" />
<br/>
<input type="password" placeholder="请输入密码" name="password" required="required" />
<br/>
<button type="submit">登录</button>
</div>
</form>
</body>
</html>
要怎么做才能让Spring Security跳转到我们自己定义的登录页面呢?很简单,只需要在BrowserSecurityConfig的configure中添加一些配置:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() // 表单登录
.loginPage("/login.html") // 登录跳转url
.loginProcessingUrl("/login") // 处理表单登录url
.and()
.authorizeRequests() // 授权配置
.antMatchers("/login.html", "/css/**").permitAll() // 无需认证
.anyRequest() // 所有请求
.authenticated() // 都需要认证
.and().csrf().disable();
}
在未登录的情况下,当用户访问html资源的时候跳转到登录页,否则返回JSON格式数据,状态码为401。要实现这个功能我们将loginPage的URL改为/authentication/require,并且在antMatchers方法中加入该URL,让其免拦截:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() // 表单登录
// .loginPage("/login.html") // 登录跳转url
.loginPage("/authentication/require")
.loginProcessingUrl("/login") // 处理表单登录url
.and()
.authorizeRequests() // 授权配置
.antMatchers("/login.html", "/css/**", "/authentication/require").permitAll() // 无需认证
.anyRequest() // 所有请求
.authenticated() // 都需要认证
.and().csrf().disable();
}
/**
* @Author chen bo
* @Date 2023/12
* @Des
*/
@RestController
public class DemoController {
private RequestCache requestCache = new HttpSessionRequestCache();
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); @GetMapping("/authentication/require")
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public String requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {
SavedRequest savedRequest = requestCache.getRequest(request, response);
if (savedRequest != null) {
String url = savedRequest.getRedirectUrl();
// 为了方便测试,我们设置只有访问url中是以.html结尾时,才会跳转登录页,其它形式的全部返回提示语(访问资源需要身份认证)。具体业务中这里可以按需求设置。
if (StringUtils.endsWithIgnoreCase(url, ".html")) {
redirectStrategy.sendRedirect(request, response, "/login.html");
}
} return "访问资源需要身份认证";
}
}
其中HttpSessionRequestCache为Spring Security提供的用于缓存请求的对象,通过调用它的getRequest方法可以获取到本次请求的HTTP信息。DefaultRedirectStrategy的sendRedirect为Spring Security提供的用于处理重定向的方法。
上面代码获取了引发跳转的请求,根据请求是否以.html为结尾来对应不同的处理方法。如果是以.html结尾,那么重定向到登录页面,否则返回”访问的资源需要身份认证!”信息,并且HTTP状态码为401(HttpStatus.UNAUTHORIZED)。
为了方便测试,添加hello.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
这是一个hello落地页
</body>
</html>
1.访问http://localhost:8080/hello的时候页面便会跳转到http://localhost:8080/authentication/require,并且输出”访问的资源需要身份认证!”
2.访问http://localhost:8090/hello.html的时候,页面将会跳转到登录页面。
v设置登录成功逻辑
要改变默认的处理成功逻辑很简单,只需要实现org.springframework.security.web.authentication.AuthenticationSuccessHandler接口的onAuthenticationSuccess方法即可:
/**
* @Author chen bo
* @Date 2023/12
* @Des
*/
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler { private RequestCache requestCache = new HttpSessionRequestCache();
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); @Autowired
private ObjectMapper objectMapper; @Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
// 默认打印出登陆信息
// httpServletResponse.setContentType("application/json;charset=utf-8");
// httpServletResponse.getWriter().write(objectMapper.writeValueAsString(authentication));
// 跳转访问页面
// SavedRequest savedRequest = requestCache.getRequest(httpServletRequest, httpServletResponse);
// redirectStrategy.sendRedirect(httpServletRequest, httpServletResponse, savedRequest.getRedirectUrl());
// 跳转制定页面
SavedRequest savedRequest = requestCache.getRequest(httpServletRequest, httpServletResponse);
redirectStrategy.sendRedirect(httpServletRequest, httpServletResponse, "hello.html");
}
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() // 表单登录
.loginPage("/login.html") // 登录跳转url
// .loginPage("/authentication/require")
.loginProcessingUrl("/login") // 处理表单登录url
.successHandler(authenticationSuccessHandler)
.and()
.authorizeRequests() // 授权配置
.antMatchers("/login.html", "/css/**", "/authentication/require").permitAll() // 无需认证
.anyRequest() // 所有请求
.authenticated() // 都需要认证
.and().csrf().disable();
}
v设置登录失败逻辑
与自定义登录成功处理逻辑类似,自定义登录失败处理逻辑需要实现org.springframework.security.web.authentication.AuthenticationFailureHandler的onAuthenticationFailure方法:
/**
* @Author chen bo
* @Date 2023/12
* @Des
*/
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler { @Autowired
private ObjectMapper objectMapper; @Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
httpServletResponse.setContentType("application/json;charset=utf-8");
httpServletResponse.getWriter().write(objectMapper.writeValueAsString(e.getMessage()));
}
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() // 表单登录
.loginPage("/login.html") // 登录跳转url
// .loginPage("/authentication/require")
.loginProcessingUrl("/login") // 处理表单登录url
.successHandler(authenticationSuccessHandler)
.failureHandler(authenticationFailureHandler)
.and()
.authorizeRequests() // 授权配置
.antMatchers("/login.html", "/css/**", "/authentication/require").permitAll() // 无需认证
.anyRequest() // 所有请求
.authenticated() // 都需要认证
.and().csrf().disable();
}
其他参考/学习资料:
- https://spring.io/projects/spring-security/
 - https://www.cnblogs.com/big-strong-yu/p/15807512.html
 - https://mrbird.cc/Spring-Security-Authentication.html
 - https://www.jianshu.com/p/b7aac6d4bc51
 
v源码地址
https://github.com/toutouge/javademosecond/tree/master/security-demo
作  者:请叫我头头哥
                
                出  处:http://www.cnblogs.com/toutou/
                
                关于作者:专注于基础平台的项目开发。如有问题或建议,请多多赐教!
                
                版权声明:本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。
                
                特此声明:所有评论和私信都会在第一时间回复。也欢迎园子的大大们指正错误,共同进步。或者直接私信我
                
                声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是作者坚持原创和持续写作的最大动力!
#comment_body_3242240 { display: none }
SpringBoot进阶教程(八十一)Spring Security自定义认证的更多相关文章
- Spring Security 自定义认证逻辑
		
Spring Security 自定义认证逻辑 这篇文章的内容基于对Spring Security 认证流程的理解,如果你不了解,可以读一下这篇文章:Spring Security 认证流程 . 分析 ...
 - SpringBoot进阶教程(六十一)intellij idea project下建多个module搭建架构(下)
		
在上一篇文章<SpringBoot进阶教程(六十)intellij idea project下建多个module(上)>中,我们已经介绍了在intellij idea中创建project之 ...
 - SpringBoot进阶教程(七十一)详解Prometheus+Grafana
		
随着容器技术的迅速发展,Kubernetes已然成为大家追捧的容器集群管理系统.Prometheus作为生态圈Cloud Native Computing Foundation(简称:CNCF)中的重 ...
 - 学习Spring Boot:(二十八)Spring Security 权限认证
		
前言 主要实现 Spring Security 的安全认证,结合 RESTful API 的风格,使用无状态的环境. 主要实现是通过请求的 URL ,通过过滤器来做不同的授权策略操作,为该请求提供某个 ...
 - Spring Security自定义认证页面(动态网页解决方案+静态网页解决方案)--练气中期圆满
		
写在前面 上一回我们简单分析了spring security拦截器链的加载流程,我们还有一些简单的问题没有解决.如何自定义登录页面?如何通过数据库获取用户权限信息? 今天主要解决如何配置自定义认证页面 ...
 - Spring Security自定义认证器
		
在了解过Security的认证器后,如果想自定义登陆,只要实现AuthenticationProvider还有对应的Authentication就可以了 Authentication 首先要创建一个自 ...
 - spring security自定义指南
		
序 本文主要研究一下几种自定义spring security的方式 主要方式 自定义UserDetailsService 自定义passwordEncoder 自定义filter 自定义Authent ...
 - Spring Security 接口认证鉴权入门实践指南
		
目录 前言 SpringBoot 示例 SpringBoot pom.xml SpringBoot application.yml SpringBoot IndexController SpringB ...
 - SpringBoot进阶教程(六十八)Sentinel实现限流降级
		
前面两篇文章nginx限流配置和SpringBoot进阶教程(六十七)RateLimiter限流,我们介绍了如何使用nginx和RateLimiter限流,这篇文章介绍另外一种限流方式---Senti ...
 - SpringBoot进阶教程(六十五)自定义注解
		
在上一篇文章<SpringBoot进阶教程(六十四)注解大全>中介绍了springboot的常用注解,springboot提供的注解非常的多,这些注解简化了我们的很多操作.今天主要介绍介绍 ...
 
随机推荐
- python语言绘图:绘制一组正态分布图
			
代码源自: https://github.com/PacktPublishing/Bayesian-Analysis-with-Python ============================= ...
 - java多线程之sleep 与 yield 区别
			
1.背景 面试中经常会被问到: sleep 与 yield 区别 2.代码 直接看代码吧! package com.ldp.demo01; import com.common.MyThreadUtil ...
 - 记一次list集合优化
			
已知某个列表List1有2000条数据,但是因为这个列表的某个字段要从另一个表查询,所以根据一个关联的查询条件查出来的另一个List2有将近75000条数据,然后需要先循环第一个List1,然后循环里 ...
 - 【Python自动化】之特殊的自动化定位操作
			
今天有时间了,想好好的把之前遇到过的自动化问题总结一下,以后有新的总结再更新 目录: 一.上传文件(4.11) 二.下拉框选择(4.11) 1.Select下拉框 2.非Select下拉框 三.下拉框 ...
 - 以MySQL为例,来看看maven-shade-plugin如何解决多版本驱动共存的问题?
			
开心一刻 清明节那天,看到一小孩在路边烧纸时不时地偷偷往火堆里扔几张考试卷子边烧边念叨:爷爷呀,你岁数大了,在那边多做做题吧,对脑子好,要是有不懂的地方,就把我老师带走,让他教您! 前提说明 假设 M ...
 - MyBatis分页实现
			
目录 分页实现 limit实现分页 RowBounds分页 分页实现 limit实现分页 为什么需要分页? 在学习mybatis等持久层框架的时候,会经常对数据进行增删改查操作,使用最多的是对数据库进 ...
 - AWS Data Analytics Fundamentals 官方课程笔记 - Variety, Veracity, Value
			
Variety structured data applications include Amazon RDS, Amazon Aurora, MySQL, MariaDB, PostgreSQL, ...
 - Git使用经验总结6-删除远端历史记录
			
删除远端的历史记录但是不影响最新的仓库内容是笔者一直想实现的功能,有两个很不错的用处: 有的历史提交不慎包含了比较敏感的信息,提交的时候没注意,过了一段时间才发现.这个时候已经有了很多新的历史提交,无 ...
 - Hash表实践 —— 两数之和
			
目录 题目背景 解题思路 题目背景 这个题目用常规的双循环就可以完成. 但不是最优解.为什么? 看看他的步骤数: N =[3,2,4] 求结果为6的两个元素坐标如下, 1). 3+2 = 5 不等于 ...
 - Google Analytics & Ads 学习笔记 2 (gtag 版本)
			
gtag 是用来取代之前的 ga 的 但其实它底层就是调用 ga 而已. 只是封装了一个上层. 1. start up script <script async src="https: ...