1. 简介
Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架,它是Spring家族中的核心一员。它基于Spring框架,为基于Java的企业应用程序提供全面的安全性解决方案。
Spring Security支持认证、授权、加密、会话管理等核心安全功能,并提供了与Spring MVC等Spring框架的无缝集成。通过简单的配置和注解,开发者可以轻松地将其集成到应用程序中,保护应用程序免受各种安全威胁。
默认情况下,Spring Security 提供了基于用户名/密码的安全验证,如下默认的登录示例:
图片
该页面是Spring Security内置的登录页面,它是基于用户名和密码的验证,要完成该认证方式其实非常的简单,我们只需要提供UserDetailsService和PasswordEncoder 两个Bean,或者是提供AuthenticationProvider 一个Bean即可。
现在我们希望扩展登录认证,添加域的登录验证,如下登录页面:
图片
该页面的登录认证,我们不仅局限于验证用户名和密码,还额外要求验证特定的域信息,以确保用户身份的全面验证与访问控制的安全性。
本篇文章我们将详细的介绍如何实现上面的认证要求。
2. 实战案例
2.1 定义实体对象&Repository
@Entity
@Table(name = "s_user")
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id ;
private String username ;
private String domain ;
private String password ;
// getters, setters
}
该实体对象实现了UserDetails接口,其主要作用是,在后续自定义UserDetailsService时,能够基于用户名及域来查询并返回相应的用户详情对象。
Repository接口定义
该接口提供一个根据用户名及域的查询方法
public interface UserRepository extends JpaRepository<User, Long> {
User findByUsernameAndDomain(String username, String domain) ;
}
接下来,我们需要定义与安全认证相关的代码了。
2.2 自定义过滤器
public class ExtraAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
ExtraHttpRequest req = new ExtraHttpRequest(request) ;
filterChain.doFilter(req, response) ;
}
}
该过滤器的作用是用来,处理登录页面的请求参数,我们需要将用户名及域两个字段进行合并处理。
public class ExtraHttpRequest extends HttpServletRequestWrapper {
public ExtraHttpRequest(HttpServletRequest request) {
super(request) ;
}
@Override
public String getParameter(String name) {
// 判断如果参数名是指定的名称,则我们将用户名与域两个表单值进行拼接
if (SecurityConfig.LOGIN_NAME_PARAMETER.equals(name)) {
String username = super.getParameter(SecurityConfig.LOGIN_NAME_PARAMETER) ;
String domain = super.getParameter(SecurityConfig.LOGIN_DOMAIN_PARAMETER) ;
return username + Character.LINE_SEPARATOR + domain ;
}
return super.getParameter(name) ;
}
}
这里拼接后,我们会在后续进行解析处理。
2.3 安全配置
@Configuration
public class SecurityConfig {
public static final String LOGIN_NAME_PARAMETER = "username" ;
public static final String LOGIN_DOMAIN_PARAMETER = "domain" ;
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Throwable {
http.csrf(csrf -> csrf.disable()) ;
http.authorizeHttpRequests(registry -> {
registry.requestMatchers("*.html", "*.css", "*.js", "/login").permitAll() ;
registry.requestMatchers("/**").authenticated() ;
}) ;
http.formLogin(form -> {
// 自定义登录页面
form.loginPage("/login").usernameParameter(LOGIN_NAME_PARAMETER) ;
}) ;
// 将我们自定义的过滤器,添加到安全过滤器链中,并且是在UsernamePasswordAuthenticationFilter
// 过滤器之前执行
http.addFilterBefore(extraAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class) ;
return http.build() ;
}
@Bean
ExtraAuthenticationFilter extraAuthenticationFilter() {
return new ExtraAuthenticationFilter() ;
}
@Bean
PasswordEncoder noopPasswordEncoder() {
return new PasswordEncoder() {
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return rawPassword != null && encodedPassword != null && rawPassword.equals(encodedPassword) ;
}
@Override
public String encode(CharSequence rawPassword) {
return rawPassword.toString() ;
}
};
}
}
下面我们会配置自定义的UserDetailsService对象,所以我们还需要提供一个PasswordEncoder类型的bean,由于我们没有对密码进行加密处理,所以我们只是做了简单的相等判断。
@Component
public class PackUserDetailsService implements UserDetailsService {
private final UserRepository userRepository ;
public PackUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
// 由于这里需要返回的是UserDetails对象,所以我们上面的User实体
// 实现了该接口
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 上面的过滤器中我们将用户与域进行了组装,所以这里将用户名进行解析处理
String[] info = StringUtils.split(username, String.valueOf(Character.LINE_SEPARATOR)) ;
return this.userRepository.findByUsernameAndDomain(info[0], info[1]) ;
}
}
再次说明:在Spring Security中,要么你提供UserDetailsService和PasswordEncoder两个Bean,要么提供一个AuthenticationProvider(通常我们可以定义DaoAuthenticationProvider即可)类型的Bean。这样就能使用自定义的逻辑进行安全认证。
2.4 自定义登录页面
我们将使用thymeleaf来编写登录页面,需要引入如下的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>
配置thymeleaf
spring:
thymeleaf:
prefix: classpath:/static/
suffix: .html
cache: false
下面是登录页面
<html lang="en">
<head>
<meta charset="UTF-8">
<title>安全登录</title>
<link href="https://cdn.bootcdn.net/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
<style type="text/css">
body {
margin: 10px auto;
}
.form-signin {
width: 50%; /* 根据需要调整宽度 */
margin: 0 auto;
padding: 20px; /* 可选,为表单添加内边距 */
border: 1px solid #ccc; /* 可选,为表单添加边框 */
border-radius: 6px; /* 可选,为表单边框添加圆角 */
}
</style>
</head>
<body>
<form class="form-signin" th:action="@{/login}" method="post">
<h2 class="form-signin-heading">安全登录</h2>
<p th:if="${param.error}" class="error">错误的用户名/域, 密码</p>
<p>
<label for="username" class="sr-only">帐号</label>
<input type="text" id="username" name="username" class="form-control" placeholder="用户名" required autofocus/>
</p>
<p>
<label for="domain" class="sr-only">域</label>
<input type="text" id="domain" name="domain" class="form-control" placeholder="登录域" required autofocus/>
</p>
<p>
<label for="password" class="sr-only">密码</label>
<input type="password" id="password" name="password" class="form-control" placeholder="密码" required autofocus/>
</p>
<button class="btn btn-sm btn-primary btn-block" type="submit">登录</button>
<a href="/index" th:href="@{/index}">返回</a>
</form>
</body>
</html>
该login.html页面保存在classpath下的/static目录中即可。
最后,我们还需要定义一个/login接口,用来跳转到上面的登录页面
@Controller
public class LoginController {
@GetMapping("/login")
public String login() {
return "login" ;
}
}
以上我们就完成了所有的代码编写。
2.4 测试
@RestController
@RequestMapping("/api")
public class ApiController {
@GetMapping("/query")
public ResponseEntity<Object> query() {
return ResponseEntity.ok("api query success") ;
}
}
数据库中的数据
图片
访问/api/query接口将跳转到登录页面
图片
成功登录后,跳回之前的页面/api/query
图片