前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Spring Security 自定义用户认证

Spring Security 自定义用户认证

作者头像
阿宝哥
发布2019-11-15 11:52:05
1.3K0
发布2019-11-15 11:52:05
举报
文章被收录于专栏:全栈修仙之路全栈修仙之路

Spring Boot 集成 Spring Security 这篇文章中,我们介绍了如何在 Spring Boot 项目中快速集成 Spring Security,同时也介绍了如何更改系统默认生成的用户名和密码。接下来本文将基于 Spring Boot 集成 Spring Security 这篇文章中所创建的项目,进一步介绍在 Spring Security 中如何实现自定义用户认证。

一、自定义认证过程

本项目所使用的开发环境及主要框架版本:

  • java version “1.8.0_144”
  • spring boot 2.2.0.RELEASE
  • spring security 5.2.0.RELEASE
1.0 配置项目 pom.xml 文件
代码语言:javascript
复制
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.0.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.semlinker</groupId>
    <artifactId>custom-user-authentication</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>custom-user-authentication</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
      
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- 省略spring-boot-starter-test、spring-security-test及spring-boot-devtools -->
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
1.1 自定义用户模型

首先创建一个 MyUser 类,用于存储模拟的用户信息(实际开发中一般从数据库中获取真实的用户信息):

代码语言:javascript
复制
// com/semlinker/domain/MyUser.java
@Data
public class MyUser implements Serializable {
    private static final long serialVersionUID = -1090551705063344205L;

    private String userName;
    private String password;
    private boolean accountNonExpired = true; // 表示账号是否未过期
    private boolean accountNonLocked = true; // 表示账号是否未锁定
    private boolean credentialsNonExpired = true; // 表示用户凭证未过期,比如用户密码
    private boolean enabled = true; // 表示用户是否启用
}
1.2 自定义 Security 配置类及 PasswordEncoder 对象

接着配置 PasswordEncoder 对象,顾名思义该对象用于密码加密。在下面的 UserDetailsService 服务中需要用到此对象,因此这里我们需要提前做好配置。PasswordEncoder 是一个密码加密接口,在 Spring Security 中有许多实现类,比如 BCryptPasswordEncoder、Pbkdf2PasswordEncoder 和 LdapShaPasswordEncoder 等。

当然我们也可以自定义 PasswordEncoder,但 Spring Security 中实现的 BCryptPasswordEncoder 功能已经足够强大,它对相同的密码进行加密后可以生成不同的结果,这样就大大提高了系统的安全性。即尽管系统中使用相同密码的某些用户不小心泄露了密码,也不会导致其他用户密码泄露。既然 BCryptPasswordEncoder 功能那么强大,我们肯定直接使用它,具体的配置方式如下:

代码语言:javascript
复制
// com/semlinker/config/WebSecurityConfig.java
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
1.3 自定义 UserDetailsService 服务

自定义 UserDetailsService 服务,需要实现 UserDetailsService 接口,该接口只包含一个 loadUserByUsername 方法,用于通过 username 来加载匹配的用户。当找不到 username 对应用户时,会抛出 UsernameNotFoundException 异常。UserDetailsService 接口的定义如下:

代码语言:javascript
复制
// org/springframework/security/core/userdetails/UserDetailsService.java
public interface UserDetailsService {
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

loadUserByUsername 方法返回 UserDetails 对象,这里的 UserDetails 也是一个接口,它的定义如下:

代码语言:javascript
复制
// org/springframework/security/core/userdetails/UserDetails.java
public interface UserDetails extends Serializable {
	Collection<? extends GrantedAuthority> getAuthorities();
	String getPassword();
	String getUsername();
	boolean isAccountNonExpired();
	boolean isAccountNonLocked();
	boolean isCredentialsNonExpired();
	boolean isEnabled();
}

顾名思义,UserDetails 表示详细的用户信息,这个接口涵盖了一些必要的用户信息字段,具体的实现类对它进行了扩展。以上方法的具体作用如下:

  • getPassword():用于获取密码;
  • getUsername():用于获取用户名;
  • isAccountNonExpired():用于判断账号是否未过期;
  • isAccountNonLocked():用于判断账号是否未锁定;
  • isCredentialsNonExpired():用于判断用户凭证是否未过期,即密码是否未过期;
  • isEnabled():用于判断用户是否可用。

介绍完上述内容,下面我们来创建一个 MyUserDetailsService 类并实现 UserDetailsService 接口,具体如下:

代码语言:javascript
复制
// com/semlinker/service/MyUserDetailsService.java
@Service
public class MyUserDetailsService implements UserDetailsService {
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        MyUser myUser = new MyUser();
        myUser.setUserName(username);
        myUser.setPassword(this.passwordEncoder.encode("hello"));

        // 使用Spring Security内部UserDetails的实现类User,来创建User对象
        return new User(username, myUser.getPassword(), myUser.isEnabled(),
                myUser.isAccountNonExpired(), myUser.isCredentialsNonExpired(),
                myUser.isAccountNonLocked(),
                AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    }
}
1.4 配置 UserDetailsService Bean 及配置 AuthenticationManagerBuilder 对象

在 Spring Security 中使用我们自定义的 MyUserDetailsService,还需要在 WebSecurityConfig 类中进行配置:

代码语言:javascript
复制
// com/semlinker/config/WebSecurityConfig.java
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    UserDetailsService myUserDetailService() {
        return new MyUserDetailsService();
    }

    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(myUserDetailService()).passwordEncoder(passwordEncoder());
    }
}

在以上 configure 方法中,我们配置了自定义的 MyUserDetailsService 和 PasswordEncoder 对象。

1.5 创建相关 Controller 及自定义登录页和首页

在 Spring Security 中 DefaultLoginPageGeneratingFilter 过滤器会为我们生成默认登录界面:

相信很多小伙伴都 “看不惯” 这个页面,下面我们就来对这个页面进行 “整容”。

HomeController 类
代码语言:javascript
复制
// com/semlinker/controller/HomeController.java
@Controller
public class HomeController {

    @GetMapping("/")
    public String index() {
        return "index";
    }

}
UserController 类
代码语言:javascript
复制
// com/semlinker/controller/UserController.java
@Controller
public class UserController {

    @GetMapping("/login")
    public String login() {
        return "login";
    }

}
index.html
代码语言:javascript
复制
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Semlinker修仙之路首页 </title>
</head>
<body>
   <h3>欢迎您来到Semlinker修仙之路首页</h3>
</body>
</html>
login.html
代码语言:javascript
复制
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>Semlinker修仙之路登录页</title>
</head>
<body>
<form class="login-form" method="post" action="/login">
    <h1>Login</h1>
    <div class="form-field">
        <i class="fas fa-user"></i>
        <input type="text" name="username" id="username" class="form-field" 
               placeholder=" " required>
        <label for="username">Username</label>
    </div>
    <div class="form-field">
        <i class="fas fa-lock"></i>
        <input type="password" name="password" id="password" class="form-field" 
               placeholder=" " required>
        <label for="password">Password</label>
    </div>
    <button type="submit" value="Login" class="btn">Login</button>
</form>
</body>
</html>
1.6 配置默认的登录页

在创建完登录页之后,还需要在 WebSecurityConfig 类中进行配置才能生效,对应的配置方式如下:

代码语言:javascript
复制
// com/semlinker/config/WebSecurityConfig.java
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    // 省略前面已设置的内容
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
            .loginPage("/login");
    }
}

完成上述配置后,我们来测试一下效果,首先启动 Spring Boot 应用,待启动完成后在浏览器中打开 http://localhost:8080/login 地址,若一切顺利的话,你将看到以下界面:

(页面来源于 https://codepen.io/alphardex/pen/zYYZorR

接下来我们来执行登录操作,这里的用户名可以是任意的,密码是前面我们所设置的 hello。但当我们输入正确的用户名和密码点击登录之后,映入眼帘的却是以下的异常页面:

代码语言:javascript
复制
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.

Mon Oct 28 14:27:25 CST 2019
There was an unexpected error (type=Forbidden, status=403).
Forbidden

这是什么原因呢?为啥被禁止访问了,小伙伴们先别急,首先打开当前项目 src/main/resources/ 目录下的 application.properties 文件,然后输入以下配置信息:

代码语言:javascript
复制
logging.level.org.springframework.security.web.FilterChainProxy=DEBUG

待完成配置之后,重启一下应用,然后重新执行一次上述的登录操作。如果没猜错的话,你重新执行登录,输入的用户名和密码也没有错,但仍看见 Whitelabel Error Page 页面。其实刚才我们已经启用的 Security FilterChainProxy 的 DEBUG 调试模式,所以我们来看一下控制台输出的异常信息:

通过上图可以发现 /login 请求,经过 CsrfFilter 过滤器就不再往下继续执行了。这里的 CsrfFilter 过滤器是用来处理跨站请求伪造攻击的过滤器,跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已登录的 Web 应用程序上执行非本意的操作的攻击方法。

现在我们已经大致知道原因了,由于我们的登录页暂不需要开启 Csrf 防御,所以我们先把 Csrf 过滤器禁用掉:

代码语言:javascript
复制
// com/semlinker/config/WebSecurityConfig.java
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .loginPage("/login")
                .and().csrf().disable();
    }
}

更新完 WebSecurityConfig 配置类,再重新跑一次前面的登录流程,这次当你点击登录之后,你将会在当前页面看到欢迎您来到Semlinker修仙之路首页这行内容。

二、处理不同类型的请求

默认情况下,当用户通过浏览器访问被保护的资源时,会默认自动重定向到预设的登录地址。这对于传统的 Web 项目来说,是没有多大问题,但这种方式就不适用于前后端分离的项目。对于前后端分离的项目,服务端一般只需要对外提供返回 JSON 格式的 API 接口。

针对上述的问题,有如下一种方案可供参考。即根据请求是否以 .html 为结尾来对应不同的处理方法。如果是以 .html 结尾,那么重定向到登录页面,否则返回 ”访问的资源需要身份认证!” 信息,并且 HTTP 状态码为401(HttpStatus.UNAUTHORIZED)。

要实现上述的功能,我们先来定义一个 WebSecurityController 类,具体实现如下:

代码语言:javascript
复制
// com/semlinker/controller/WebSecurityController.java
@Slf4j
@RestController
public class WebSecurityController {
    // 原请求信息的缓存及恢复
    private RequestCache requestCache = new HttpSessionRequestCache();

    // 用于执行重定向操作
    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    /**
     * 默认的登录页,用于处理不同的登录认证逻辑
     *
     * @param request
     * @param response
     * @return
     */
    @RequestMapping("/authentication/require")
    @ResponseStatus(code = HttpStatus.UNAUTHORIZED)
    public String requireAuthenication(HttpServletRequest request, 
      HttpServletResponse response) throws Exception {
        SavedRequest savedRequest = requestCache.getRequest(request, response);
        if (savedRequest != null) {
            String targetUrl = savedRequest.getRedirectUrl();
            log.info("引发跳转的请求是:" + targetUrl);
            if (StringUtils.endsWithIgnoreCase(targetUrl, ".html")) {
                redirectStrategy.sendRedirect(request, response, "/login.html");
            }
        }

        return "访问的服务需要身份认证,请引导用户到登录页";
    }
}

接着将 formLogin 的默认登录页,修改为 /authentication/require,并通过 antMatchers 方法设置免拦截:

代码语言:javascript
复制
// com/semlinker/config/WebSecurityConfig.java
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .loginPage("/authentication/require")
                .and()
                .authorizeRequests()
                .antMatchers("/authentication/require", "/login.html").permitAll()
                .anyRequest().authenticated()
                .and().csrf().disable()
        ;
    }
}

同时也要修改一下前面定义的 UserController 类,让其支持 /login.html 路径映射:

代码语言:javascript
复制
// com/semlinker/controller/UserController.java
@Controller
public class UserController {

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

}

完成上述调整后,到我们访问 http://localhost:8080/index 的时候,页面会自动跳转到 http://localhost:8080/authentication/require,并且输出 “访问的服务需要身份认证,请引导用户到登录页”。而当我们访问 http://localhost:8080/index.html 的时候,页面会跳转到登录页面。

三、自定义处理登录成功和失败逻辑

在前后端分离项目中,当用户登录成功或登录失败时,需要向前端返回相应的信息,而不是直接进行页面跳转。针对前后端分离的场景,可以利用 Spring Security 中的 AuthenticationSuccessHandlerAuthenticationFailureHandler 这两个接口或继承 SimpleUrlAuthenticationSuccessHandlerSimpleUrlAuthenticationFailureHandler 类来实现自定义登录成功和登录失败的处理逻辑。

3.1 自定义登录成功处理逻辑

这里我们选用继承 SimpleUrlAuthenticationSuccessHandler 类,来实现自定义登录成功处理逻辑:

代码语言:javascript
复制
// com/semlinker/handler/MyAuthenctiationSuccessHandler.java
@Slf4j
@Component
public class MyAuthenctiationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
      HttpServletResponse response, Authentication authentication)
        throws IOException, ServletException {

        log.info("登录成功");
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(authentication));
    }
}
3.2 自定义登录失败处理逻辑

同样我们也选用继承 SimpleUrlAuthenticationFailureHandler 类,来实现自定义登录失败处理逻辑:

代码语言:javascript
复制
// com/semlinker/handler/MyAuthenctiationFailureHandler.java
@Slf4j
@Component
public class MyAuthenctiationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, 
      HttpServletResponse response,AuthenticationException exception) 
        throws IOException, ServletException {

        log.info("登录失败");
        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(exception.getMessage()));
    }
}
3.3 配置 MyAuthenctiationSuccessHandler 和 MyAuthenctiationFailureHandler

最后要让自定义处理登录成功和失败逻辑生效,还需要在 WebSecurityConfig 类中配置 FormLoginConfigurer 对象的 successHandler 和 failureHandler 属性,到目前为止 WebSecurityConfig 类的完整配置如下:

代码语言:javascript
复制
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private MyAuthenctiationFailureHandler myAuthenctiationFailureHandler;

    @Autowired
    private MyAuthenctiationSuccessHandler myAuthenctiationSuccessHandler;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    UserDetailsService myUserDetailService() {
        return new MyUserDetailsService();
    }

    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(myUserDetailService()).passwordEncoder(passwordEncoder());
    }

    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .loginPage("/login")
                .successHandler(myAuthenctiationSuccessHandler)
                .failureHandler(myAuthenctiationFailureHandler)
                .and()
                .authorizeRequests()
                .antMatchers("/authentication/require", "/login").permitAll()
                .anyRequest().authenticated()
                .and().csrf().disable()
        ;
    }
}

前面本文已经介绍了在 Spring Security 中实现自定义用户认证的流程,在学习过程中如果小伙伴们遇到其它问题的话,建议可以开启 FilterChainProxy 的 DEBUG 模式进行日志排查。

本文项目地址:Github - custom-user-authentication

四、参考资源

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019/11/14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、自定义认证过程
    • 1.0 配置项目 pom.xml 文件
      • 1.1 自定义用户模型
        • 1.2 自定义 Security 配置类及 PasswordEncoder 对象
          • 1.3 自定义 UserDetailsService 服务
            • 1.4 配置 UserDetailsService Bean 及配置 AuthenticationManagerBuilder 对象
              • 1.5 创建相关 Controller 及自定义登录页和首页
                • HomeController 类
                • UserController 类
                • index.html
                • login.html
              • 1.6 配置默认的登录页
              • 二、处理不同类型的请求
              • 三、自定义处理登录成功和失败逻辑
                • 3.1 自定义登录成功处理逻辑
                  • 3.2 自定义登录失败处理逻辑
                    • 3.3 配置 MyAuthenctiationSuccessHandler 和 MyAuthenctiationFailureHandler
                    • 四、参考资源
                    领券
                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档