@[toc]
个别状况下,咱们在应用 Spring Security 的时候,用的是 Spring Security 自带的登录计划,配置一下登录接口,配置一下登录参数,再配置一下登录回调就能用了,这种用法能够算是最佳实际了!

然而!

总会有一些奇奇怪怪得需要,例如想自定义登录,像 Shiro 那样本人写登录逻辑,如果要实现这一点,该怎么做?明天松哥就来和大家分享一下。

松哥推敲了一下,想在 Spring Security 中自定义登录逻辑,咱们有两种思路,不过这两种思路底层实现其实殊途同归,咱们一起来看下。

1. 化腐朽为神奇

后面松哥和大家分享了一个 Spring Security 视频:

  • 没见过的奇葩登录

这个视频里次要是和大家分享了咱们其实能够应用 HttpServletRequest 来实现零碎的登录,这其实是 JavaEE 的标准,这种登录形式尽管冷门,然而却很好玩!

而后松哥还和大家分享了一个视频:

  • SpringSecurity登录数据获取最初一讲

这个视频其实是在讲 Spring Security 对 HttpServletRequest 登录逻辑的实现,或句话说,HttpServletRequest 中提供的那几个和登录相干的 API,Spring Security 都依照本人的实现形式对其进行了重写。

有了这两个储备常识后,第一个 DIY Spring Security 登录的计划跃然纸上。

1.1 实际

咱们来看看具体操作。

首先咱们来创立一个 Spring Boot 工程,引入 Web 和 Security 两个依赖,如下:

不便起见,咱们在 application.properties 中配置一下默认的用户名明码:

spring.security.user.name=javaboyspring.security.user.password=123

接下来咱们提供一个 SecurityConfig,为登录接口放行:

@Configurationpublic class SecurityConfig extends WebSecurityConfigurerAdapter {    @Override    protected void configure(HttpSecurity http) throws Exception {        http.authorizeRequests()                .antMatchers("/login")                .permitAll()                .anyRequest().authenticated()                .and()                .csrf().disable();    }}

登录接口就是 /login,一会咱们自定义的登录逻辑就写在这个里边,咱们来看下:

@RestControllerpublic class LoginController {    @PostMapping("/login")    public String login(String username, String password, HttpServletRequest req) {        try {            req.login(username, password);            return "success";        } catch (ServletException e) {            e.printStackTrace();        }        return "failed";    }}

间接调用 HttpServletRequest#login 办法,传入用户名和明码实现登录操作。

最初咱们再提供一个测试接口,如下:

@RestControllerpublic class HelloController {    @GetMapping("/hello")    public String hello() {        return "hello security!";    }}

just this!

启动我的项目,咱们首先拜访 /hello 接口,会拜访失败,接下来咱们拜访 /login 接口执行登录操作,如下:

登录胜利之后,再去拜访 /hello 接口,此时就能够拜访胜利了。

是不是很 Easy?登录胜利后,当前的受权等操作都还是原来的写法不变。

1.2 原理剖析

下面这种登录形式的原理其实松哥一开始就介绍过了,如果大家还不相熟,能够看看这两个视频就懂了:

  • 没见过的奇葩登录
  • SpringSecurity登录数据获取最初一讲

这里我也是略微说两句。

咱们在 LoginController#login 办法中所获取到的 HttpServletRequest 实例其实是 HttpServlet3RequestFactory 中的一个外部类 Servlet3SecurityContextHolderAwareRequestWrapper 的对象,在这个类中,重写了 HttpServletRequest 的 login 以及 authenticate 等办法,咱们先来看看 login 办法,如下:

@Overridepublic void login(String username, String password) throws ServletException {    if (isAuthenticated()) {        throw new ServletException("Cannot perform login for '" + username + "' already authenticated as '"                + getRemoteUser() + "'");    }    AuthenticationManager authManager = HttpServlet3RequestFactory.this.authenticationManager;    if (authManager == null) {        HttpServlet3RequestFactory.this.logger.debug(                "authenticationManager is null, so allowing original HttpServletRequest to handle login");        super.login(username, password);        return;    }    Authentication authentication = getAuthentication(authManager, username, password);    SecurityContextHolder.getContext().setAuthentication(authentication);}

能够看到:

  1. 如果用户曾经认证了,就抛出异样。
  2. 获取到一个 AuthenticationManager 对象。
  3. 调用 getAuthentication 办法实现登录,在该办法中,会依据用户名明码构建 UsernamePasswordAuthenticationToken 对象,而后调用 Authentication#authenticate 办法实现登录,具体代码如下:
private Authentication getAuthentication(AuthenticationManager authManager, String username, String password)        throws ServletException {    try {        return authManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));    }    catch (AuthenticationException ex) {        SecurityContextHolder.clearContext();        throw new ServletException(ex.getMessage(), ex);    }}
该办法返回的是一个认证后的 Authentication 对象。
  1. 最初,将认证后的 Authentication 对象存入 SecurityContextHolder 中,这里的具体逻辑我就不啰嗦了,我在公众号【江南一点雨】之前的视频中曾经讲过屡次了。

这就是 login 办法的执行逻辑。

Servlet3SecurityContextHolderAwareRequestWrapper 类也重写了 HttpServletRequest#authenticate 办法,这个也是做认证的办法:

@Overridepublic boolean authenticate(HttpServletResponse response) throws IOException, ServletException {    AuthenticationEntryPoint entryPoint = HttpServlet3RequestFactory.this.authenticationEntryPoint;    if (entryPoint == null) {        HttpServlet3RequestFactory.this.logger.debug(                "authenticationEntryPoint is null, so allowing original HttpServletRequest to handle authenticate");        return super.authenticate(response);    }    if (isAuthenticated()) {        return true;    }    entryPoint.commence(this, response,            new AuthenticationCredentialsNotFoundException("User is not Authenticated"));    return false;}

能够看到,这个办法用来判断用户是否曾经实现认证操作,返回 true 示意用户曾经实现认证,返回 false 示意用户尚未实现认证工作。

2. 源码的力量

看了下面的原理剖析,大家应该也明确了第二种计划了,就是不应用 HttpServletRequest#login 办法,咱们间接调用 AuthenticationManager 进行登录验证。

一起来看下。

首先咱们批改配置类如下:

@Configurationpublic class SecurityConfig extends WebSecurityConfigurerAdapter {    @Override    protected void configure(HttpSecurity http) throws Exception {        http.authorizeRequests()                .antMatchers("/login","/login2")                .permitAll()                .anyRequest().authenticated()                .and()                .csrf().disable();    }    @Override    @Bean    public AuthenticationManager authenticationManagerBean() throws Exception {        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();        manager.createUser(User.withUsername("javaboy").password("{noop}123").roles("admin").build());        provider.setUserDetailsService(manager);        return new ProviderManager(provider);    }}
  1. 首先在登录放行中,增加 /login2 接口,这是我行将自定义的第二个登录接口。
  2. 提供一个 AuthenticationManager 实例,对于 AuthenticationManager 的玩法松哥在之前的 Spring Security 系列中曾经屡次分享过,这里就不再赘述(没看过的小伙伴公众号后盾回复 ss)。创立 AuthenticationManager 实例时,还须要提供一个 DaoAuthenticationProvider,大家晓得,用户明码的校验工作在这个类里边实现,并为 DaoAuthenticationProvider 配置一个 UserDetailsService 实例,该实体提供了用户数据源。

接下来提供一个登录接口:

@RestControllerpublic class LoginController {    @Autowired    AuthenticationManager authenticationManager;    @PostMapping("/login2")    public String login2(String username, String password, HttpServletRequest req) {        try {            Authentication token = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));            SecurityContextHolder.getContext().setAuthentication(token);            return "success";        } catch (Exception e) {            e.printStackTrace();        }        return "failed";    }}

在登录接口中,传入用户名明码等参数,而后将用户名明码等参数封装成一个 UsernamePasswordAuthenticationToken 对象,最初调用 AuthenticationManager#authenticate 办法进行验证,验证胜利后会返回一个认证后的 Authentication 对象,再手动把该 Authentication 对象存入 SecurityContextHolder 中。

配置实现后,重启我的项目,进行登录测试即可。

第二种计划和第一种计划殊途同归,第二种实际上就是把第一种的底层拉进去本人从新实现,仅此而已

3. 小结

好啦,明天就和大家介绍了两种 Spring Security DIY 登录的计划,这些计划可能工作中并不罕用,然而对于大家了解 Spring Security 原理还是大有裨益的,感兴趣的小伙伴能够敲一下试试哦~

另外,如果你感觉浏览本文吃力,无妨在公众号后盾回复 ss,看看 Spring Security 系列的其余文章,这有助于了解本文,当然也能够看看松哥的新书:

《深入浅出Spring Security》一书已由清华大学出版社正式出版发行,感兴趣的小伙伴戳这里->->>深入浅出Spring Security,一本书学会 Spring Security。