内容纲要

本章的内容较为简单,暂时先不涉及数据库的操作,只是为了演示,在Spring Security 中,如何通过其暴露的接口来自定义处理用户登录过程的逻辑。
想要了解用户登录的逻辑,我们只需要处理三个问题:

  • a.用户信息获取逻辑
  • b.如何校验用户信息
  • c.处理用户加密解密

spring boot教程

1.spring security 用户信息获取 UserDetailsService

用户信息的获取逻辑在Spring Security 中是被封装在一个叫UserDetailsService的接口里的,在这个接口中,只有一个方法,loadUserByUsername()。接口接收一个String参数,返回一个UserDetails对象。顾名思义,就是要传递我们的username,然后根据这个username去找到这个User的用户信息。如果找不到这个用户,就会抛出一个UsernameNotFoundException,Spring Security 捕获到这个异常,就会返回相应的错误信息。

package org.springframework.security.core.userdetails;

public interface UserDetailsService {
    UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}

接下来,我们自己写一个类,实现这个UserDetailsService

@Component
public class MyUserDetailsService implements UserDetailsService {

    private static final Logger logger = LoggerFactory.getLogger(MyUserDetailsService.class);

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        logger.info("登录的用户名:" + username);
        return new User("admin","123456", AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    }
}

我们调用了这个loadUserByUsername方法,要返回的是UserDetails对象。可以看到代码中我是直接返回一个Spring Security中的User对象。这个User对象的源码我们来看看,它提供了两种构造方法

/**
 * 这个构造方法三个参数:username代表用户名,password代表密码,第三个参数 authorities表示用户权限的集合。而这个方法具体实现则是调用了另一个构造方法。并且多了四个值为true的参数。接下来看另一个构造方法。
 */
 public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
        this(username, password, true, true, true, true, authorities);
    }
/**
 * enabled:用户是否启用,accountNonExpired:用户是否过期,
 * credentialsNonExpired:用户凭证是否过期,accountNonLocked:用户是否被锁定。
 */
public User(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
        if (username != null && !"".equals(username) && password != null) {
            this.username = username;
            this.password = password;
            this.enabled = enabled;
            this.accountNonExpired = accountNonExpired;
            this.credentialsNonExpired = credentialsNonExpired;
            this.accountNonLocked = accountNonLocked;
            this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
        } else {
            throw new IllegalArgumentException("Cannot pass null or empty values to constructor");
        }
    }

2.用户信息校验 UserDetails

User对象和UserDetails又是什么关系呢? 现在一起来看看UserDetails对象的源码实现

public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();

    String getPassword();

    String getUsername();

    boolean isAccountNonExpired();

    boolean isAccountNonLocked();

    boolean isCredentialsNonExpired();

    boolean isEnabled();
}

UserDetails是个接口(interface),那难道说是User对象实现了UserDetails接口吗?答案是肯定的。

public class User implements UserDetails, CredentialsContainer {
    // 省略代码
}

isAccountNonExpired(标识用户是否过期了,也就是用户是否被冻结,有些应用可能需要)
isAccountNonLocked(标识用户是否被锁定,一般用于用户输入密码错误次数的验证)
isCredentialsNonExpired(标识用户密码是否过期,有些网站为了安全性起见,可能会对用户密码的使用时间做验证)
isEnabled(用户是否启用,一般来说,对于应用,我们是不会删除用户数据的,如果用户注销的话,我们只需标识用户不可用就好了)
如果以上判断中,并不需要用到,那只要返回true就好了。

3.用户信息加解密 PasswordEncoder

再提一下用户对用户密码加解密的方法: PasswordEncoder。
先来写个配置类

@Configuration
@EnableWebSecurity // 启用Spring Security
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    // 注入 MyUserDetailsSerivce
    @Autowired
    private MyUserDetailsService userDetailsService;
    // 创建PasswordEncoder Bean
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 设置form 登录方式,且成功跳转至hello页面
        http.formLogin().successForwardUrl("/hello")
                // 设置所有的请求都要经过验证
              .and().authorizeRequests().anyRequest().authenticated()
               // 设置自定义的UserDetailsService
              .and().userDetailsService(userDetailsService);

    }
}

OK, 接下来我们稍微对我们自定义的UserDetailsService实现做些变化,利用passwordEncoder来加密我们的密码。

@Component
public class MyUserDetailsService implements UserDetailsService {
    private static final Logger logger = LoggerFactory.getLogger(MyUserDetailsService.class);

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        logger.info("登录的用户名:" + username);
        String password = passwordEncoder.encode("123456");
        logger.info("加密后的密码:" + password);
        return new User("admin", password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    }
}

接着,写一个简单的controller

/**
 * @author developlee
 * @since 2018/11/25 20:40
 */
@RestController
public class HelloController {
    @PostMapping("/hello")
    public String hello(){
        return "Hello ! My Dear Friend !";
    }
}

做个简单的验证,让我们来访问下,http:localhost:8080/hello

spring boot教程

跳转至Spring Security为我们提供登录页面。
输入用户名密码

spring boot教程
后台日志打印结果截图
spring boot教程

可以看到, 每次加密过的密码都是不一样的。这里面的机制,其实就是密码中常用的加盐方式。

4.总结

这篇算是真正踏入Spring Security的入门,了解了Spring Security 中最基本也是几个类及方法,User,UserDetails,UserDetailsService,以及PasswordEncoder。
接下来的文章中,将更加深入去探讨Spring Security的登录流程,权限控制等。

以上代码均可在我的github.com中找到,感谢花时间阅读,如果文章中有错误或不足之处,烦请赐教,共同探讨。

By liu luli

8年IT行业从业经验,参与、负责过诸多大型项目建设。掌握多门编程语言,对Java、Python编程有较为深刻的理解。现为杭州某公司开发负责人。

One thought on “精通Spring Boot: Spring Security自定义登录”

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注