个性化认证!Spring Security 轻松扩展登录字段

开发 前端
Spring Security支持认证、授权、加密、会话管理等核心安全功能,并提供了与Spring MVC等Spring框架的无缝集成。通过简单的配置和注解,开发者可以轻松地将其集成到应用程序中,保护应用程序免受各种安全威胁。

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

图片图片

责任编辑:武晓燕 来源: Spring全家桶实战案例源码
相关推荐

2020-06-28 07:00:00

推荐系统智能商务服务平台

2023-09-25 15:54:28

Canvas国庆

2024-10-05 00:00:25

Cursor网站代码

2022-11-01 07:19:45

推荐系统非个性化

2011-01-20 10:19:21

PowerShell个性化

2011-05-04 14:38:53

海尔江山帝景一体机

2013-11-07 16:42:34

Windows 8.1个性化

2023-03-21 12:46:30

智慧城市人工智能大数据

2020-08-31 12:00:17

Linux终端颜色命令

2009-07-13 15:33:24

桌面虚拟化虚拟化IT

2017-11-22 09:24:00

2011-04-28 11:14:33

simpleframe

2013-01-04 09:41:11

云计算个性化精准促销Me Marketin

2021-10-19 08:00:00

Windows 11Windows微软

2023-07-26 07:51:30

游戏中心个性化

2018-10-25 14:21:22

Oracle数字化助手

2024-01-16 15:51:55

个性化图片3D

2010-04-30 17:07:03

组策略部署

2011-08-18 18:53:30

win7
点赞
收藏

51CTO技术栈公众号