Spring Security 实现自定义用户认证方案可以根据具体需求和业务场景进行设计和实施,满足不同的安全需求和业务需求。这种灵活性使得认证机制能够更好地适应各种复杂的环境和变化。通过自定义认证方案,可以更好地控制和管理用户的访问权限,确保数据和应用程序的安全性和可靠性。
基于 Spring Security 自定义用户认证方案的开发流程如下图:
UserDetails 接口代表用户详细信息,而负责对 UserDetails 进行各种操作的则是 UserDetailsService 接口。因此,实现自定义用户认证方案首先要做的是实现 UserDetails 和 UserDetailsService 接口。同时,如果扩展了用信息,可以结合 AuthenticationProvider 接口来扩展整个认证流程。
1、SpringBoot 整合 SpringSecurity 框架
【示例】SpringBoot 整合 SpringSecurity 创建一个自定义用户认证应用。
1.1 创建 Spring Boot 项目
创建 SpringBoot 项目,项目结构如下图:
1.2 添加 Maven 依赖
在 pom.xml 配置文件中添加 Spring Security、Thymeleaf 模板引擎、Lombok 依赖。
<!-- Spring Security 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.7.18</version>
</dependency>
<!-- Lombok 依赖 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 引入Thymeleaf模板引擎 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
2、Spring Security 实现自定义用户认证方案
Spring Security 所做的工作只是把常见的、符合一般业务场景的实现方法进行抽象并嵌入框架中,开发人员完全可以自定义用户认证方案。
2.1 扩展 UserDetails
扩展 UserDetails 的方法是直接实现该接口。
package com.pjb.model;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
/**
* 自定义用户认证类:扩展 UserDetails 接口
* @author pan_junbiao
**/
@Data
public class LoginUserDetails implements UserDetails
{
private Long userId;
private String username;
private String password;
private String BlogName; //博客名称
private String BlogUrl; //博客地址
private List<GrantedAuthority> authoritys; //权限集合
@Override
public Collection<? extends GrantedAuthority> getAuthorities()
{
return this.authoritys;
}
public void setAuthoritys(List<GrantedAuthority> authoritys)
{
this.authoritys = authoritys;
}
@Override
public boolean isAccountNonExpired()
{
return true;
}
@Override
public boolean isAccountNonLocked()
{
return true;
}
@Override
public boolean isCredentialsNonExpired()
{
return true;
}
@Override
public boolean isEnabled()
{
return true;
}
}
2.2 扩展 UserDetailsService
接下来我们实现 UserDetailsService 接口。
package com.pjb.service;
import com.pjb.model.LoginUserDetails;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 登录服务类:扩展 UserDetailsService 接口
* @author pan_junbiao
**/
@Service
public class LoginUserDetailsService implements UserDetailsService
{
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
//模拟数据库查询
LoginUserDetails user = this.findByUsername(username);
if (user != null)
{
return user;
}
//未查询到用户信息,则抛出异常
throw new UsernameNotFoundException("未找到用户名称为:" + username + "的用户信息");
}
/**
* 模拟数据库查询
*/
private LoginUserDetails findByUsername(String username)
{
LoginUserDetails loginUserDetails = null;
if (username != null && username.equals("panjunbiao"))
{
loginUserDetails = new LoginUserDetails();
loginUserDetails.setUserId(1L);
loginUserDetails.setUsername(username);
loginUserDetails.setPassword("123456");
loginUserDetails.setBlogName("您好,欢迎访问 pan_junbiao的博客");
loginUserDetails.setBlogUrl("https://blog.csdn.net/pan_junbiao");
//设置权限
List<GrantedAuthority> grantedAuthorityList = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER");
loginUserDetails.setAuthoritys(grantedAuthorityList);
}
return loginUserDetails;
}
}
2.3 扩展 AuthenticationProvider
扩展 AuthenticationProvider 是实现自定义认证流程的最后一步,这个过程提供一个自定义的 AuthenticationProvider 实现类。
package com.pjb.provider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
/**
* 登录认证类:扩展 AuthenticationProvider
* @author pan_junbiao
**/
@Component
public class LoginAuthenticationProvider implements AuthenticationProvider
{
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException
{
//从 Authentication 对象中获取用户名称和身份凭证信息
String username = authentication.getName();
String password = authentication.getCredentials().toString();
UserDetails user = userDetailsService.loadUserByUsername(username);
if (passwordEncoder.matches(password, user.getPassword()))
{
//密码匹配成功,则构建一个 UsernamePasswordAuthenticationToken 对象并返回
return new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), user.getAuthorities());
} else
{
//密码匹配失败,则抛出异常
throw new BadCredentialsException("用户密码不正确");
}
}
@Override
public boolean supports(Class<?> authenticationType)
{
return authenticationType.equals(UsernamePasswordAuthenticationToken.class);
}
}
3、Spring Security 的处理类
3.1 登录成功处理类
package com.pjb.handler;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 登录成功处理类
*/
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler
{
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException
{
//重定向至首页
httpServletResponse.sendRedirect("/");
}
}
3.2 登录失败处理类
package com.pjb.handler;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* 登录失败处理类
*/
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler
{
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException authenticationException) throws IOException, ServletException
{
//获取登录失败原因
String errorMessage = "";
if(authenticationException instanceof BadCredentialsException){
errorMessage = "用户名或密码不正确";
}else if(authenticationException instanceof DisabledException){
errorMessage = "账号被禁用";
}else if(authenticationException instanceof UsernameNotFoundException){
errorMessage = "用户名不存在";
}else if(authenticationException instanceof CredentialsExpiredException){
errorMessage = "密码已过期";
}else if(authenticationException instanceof LockedException) {
errorMessage = "账号被锁定";
}else{
errorMessage = "未知异常";
}
//设置响应编码
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter out = httpServletResponse.getWriter();
out.write(errorMessage);
}
}
3.3 403无权限处理类
package com.pjb.handler;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* 403无权限处理类
*/
@Component
public class PermissionDeniedHandler implements AccessDeniedHandler
{
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException
{
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter out = httpServletResponse.getWriter();
out.write("403无权限");
}
}
4、Spring Security 的核心配置类
创建 WebSecurityConfig 类(Spring Security 配置类),并添加 @EnableWebSecurity 注解和继承 WebSecurityConfigurerAdapter 类。
package com.pjb.config;
import com.pjb.handler.LoginFailureHandler;
import com.pjb.handler.LoginSuccessHandler;
import com.pjb.handler.PermissionDeniedHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* Spring Security 配置类
* @author pan_junbiao
**/
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter
{
@Autowired
private LoginSuccessHandler loginSuccessHandler;
@Autowired
private LoginFailureHandler loginFailureHandler;
@Autowired
private PermissionDeniedHandler permissionDeniedHandler;
//自定义的扩展 UserDetailsService 类
@Autowired
private UserDetailsService loginUserDetailsService;
//自定义的扩展 AuthenticationProvider 类
@Autowired
private AuthenticationProvider loginAuthenticationProvider;
@Override
protected void configure(HttpSecurity http) throws Exception
{
http.authorizeRequests() //返回一个URL拦截注册器
.anyRequest() //匹配所有的请求
.authenticated() //所有匹配的URL都需要被认证才能访问
.and() //结束当前标签,让上下文回到 HttpSecurity
.formLogin() //启动表单认证
.loginPage("/myLogin.html") //自定义登录页面
.loginProcessingUrl("/auth/form") //指定处理登录请求路径
.permitAll() //使登录页面不设限访问
//.defaultSuccessUrl("/index") //登录认证成功后的跳转页面
.successHandler(loginSuccessHandler) //指定登录成功时的处理
.failureHandler(loginFailureHandler) //指定登录失败时的处理
.and()
.exceptionHandling().accessDeniedHandler(permissionDeniedHandler) //403无权时的返回操作
.and().csrf().disable(); //关闭CSRF的防御功能
}
/**
* 核心代码:
* 将扩展的 LoginUserDetailsService 和 LoginAuthenticationProvider 注入,
* 并将其添加到 AuthenticationManagerBuilder 中,这样 AuthenticationManagerBuilder 将基于上述
* 自定义的 LoginUserDetailsService 来完成 UserDetails 的创建和管理,
* 并基于自定义的 LoginAuthenticationProvider 完成用户认证。
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
{
auth.userDetailsService(loginUserDetailsService);
auth.authenticationProvider(loginAuthenticationProvider);
}
/**
* 由于5.x版本之后默认启用了委派密码编译器,
* 因而按照以往的方式设置内存密码将会读取异常,
* 所以需要暂时将密码编码器设置为 NoOpPasswordEncoder
*/
@Bean
public PasswordEncoder passwordEncoder()
{
return NoOpPasswordEncoder.getInstance();
}
}
将扩展的 LoginUserDetailsService 和 LoginAuthenticationProvider 注入,并将其添加到 AuthenticationManagerBuilder 中,这样 AuthenticationManagerBuilder 将基于上述自定义的 LoginUserDetailsService 来完成 UserDetails 的创建和管理,并基于自定义的 LoginAuthenticationProvider 完成用户认证。
5、前端页面
5.1 控制器层
创建 IndexController 类(首页控制器),实现获取当前登录用户名并跳转至首页。
package com.pjb.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
import java.security.Principal;
/**
* 首页控制器
* @author pan_junbiao
**/
@Controller
public class IndexController
{
/**
* 首页
*/
@RequestMapping("/")
public String index(HttpServletRequest request)
{
//获取当前登录人
String userName = "未登录";
Principal principal = request.getUserPrincipal();
if (principal != null)
{
userName = principal.getName();
}
//返回页面
request.setAttribute("userName", userName);
return "/index.html";
}
}
5.2 编写登录页面
在 resources\static 静态资源目录下,创建 myLogin.html 页面。
注意:myLogin.html 页面必须放在 resources\static 静态资源目录下,否则页面无法加载。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
<meta name="author" content="pan_junbiao的博客">
</head>
<body>
<form name="myForm" action="/auth/form" method="post">
<table align="center">
<caption>用户登录</caption>
<tr>
<td>登录账户:</td>
<td>
<input type="text" name="username" placeholder="请输入登录账户" value="panjunbiao" />
</td>
</tr>
<tr>
<td>登录密码:</td>
<td>
<input type="password" name="password" placeholder="请输入登录密码" value="123456" />
</td>
</tr>
<!-- 以下是提交、取消按钮 -->
<tr>
<td colspan="2" style="text-align: center; padding: 5px;">
<input type="submit" value="提交" />
<input type="reset" value="重置" />
</td>
</tr>
</table>
</form>
</body>
</html>
5.3 编写首页
在 resources\templates 资源目录下,创建 index.html 页面。
注意:首页 index.html 页面中使用 Thymeleaf 模板 。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>首页</title>
<meta name="author" content="pan_junbiao的博客">
</head>
<body>
<h1 style="color: red">Hello,Spring Security</h1>
<p>博客信息:您好,欢迎访问 pan_junbiao的博客</p>
<p>博客地址:https://blog.csdn.net/pan_junbiao</p>
<p th:text="'当前登录人:' + ${userName}"></p>
<a href="/logout" onclick="return confirm('确认注销吗?');">登出</a>
</body>
</html>