Аутентификация входа на основе формы
1. Аутентификация безопасности по умолчанию
当我们项目里添加spring security依赖它就已经起作用了,启动项目访问时,会出现弹出框。spring security默认
采用basic模式认证。浏览器发送http报文请求一个受保护的资源,浏览器会弹出对话框让输入用户名和密码。并以用
户名:密码的形式base64加密,加入Http报文头部的Authorization(默认用户名为user,密码则是会在启动程序时后
台console里输出,每次都不一样)。后台获取Http报文头部相关认证信息,认证成功返回相应内容,失败则继续认证。
下面会详细介绍具体认证流程。
Во-вторых, принцип проверки подлинности на основе формы
基本认证流程:
SecurityContextPersistenceFilter
↓
AbstractAuthenticationProcessingFilter
↓
UsernamePasswordAuthenticationFilter
↓
AuthenticationManager
↓
AuthenticationProvider
↓
userDetailsService
↓
userDetails
↓
认证通过
↓
SecurityContext
↓
SecurityContextHolder
↓
RememberMeServices
↓
AuthenticationSuccessHandler
SecurityContextPersistenceFilter会校验请求中session是否有SecurityContext,有放SecurityContextHolder
中,返回时校验SecurityContextHolder中是否有securityContext,有放session,从而实现认证信息在多个请求中共
享。
AbstractAuthenticationProcessingFilter中会调自身attemptAuthentication抽象方法,
UsernamePasswordAuthenticationFilter是其一个实现类。
它从请求中获取账号密码后,构造了一个token,此时没有认证,随后会调用AuthenticationManager接口的
authenticate方法
下面是其构造函数,我们可看到这个token没有认证
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super((Collection)null);
this.principal = principal;
this.credentials = credentials;
this.setAuthenticated(false);
}
ProviderManager实现了AuthenticationManager接口,它获取所有provider后遍历调用其supports方法,去匹配哪
个provider支持之前申明的token,找到provider后调用authenticate方法。
到provider才开始认证用户信息,表单登录的是DaoAuthenticationProvider其父类进行以下操作:
①调用userDetailsService接口的loadUserByUsername方法获取UserDetails用户信息
②检查UserDetails用户是否可用、是否账户没有过期、是否账户没有被锁定
③检查UserDetails密码是否正确
④检查UserDetails密码是否没有过期
⑤都通过会重新申明一个token,不过多了权限集合参数如下
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}
此时用户才被认证通过,上述步骤有一个错了会抛出AuthenticationException异常的子类,下面是其继承图:
认证结果返回给AbstractAuthenticationProcessingFilter,将结果放到SecurityContextHolder的
SecurityContext中,若添加了记住我功能又会调用rememberMeServices来实现,最后调用
AuthenticationSuccessHandler接口成功处理。
3. Реализация формы входа
在第二部分介绍具体原理,接下来实现自定义登录验证。首先编写一个config类继承WebSecurityConfigurerAdapter
类重写configure方法填写配置
protected void configure(HttpSecurity http) throws Exception {
http
/**
* 表单登录配置
*/
.formLogin() //表单登录
.loginPage("/authentication/login.html") //自定义登录页面
.loginProcessingUrl("/authentication/form") //与自定义登录页面处理路径一致
// http.httpBasic() basic模式弹出框登录
.successHandler(myAuthenticationSuccessHandler) //自定义认证成功处理
.failureHandler(myAuthenticationFailureHandler) //自定义认证失败处理
.and()
/**
* 需要认证的请求配置,
* 注:最为具体的请求路径放在前面,而最不具体的路径(如anyRequest())放在最后面。
* 如果不这样做的话,那不具体的路径配置将会覆盖掉更为具体的路径配置
*/
.authorizeRequests()
//允许这样请求通过,需要将登录所需路径配好,不然会一直重定向
.antMatchers("/authentication/*").permitAll()
.anyRequest().authenticated()//任何请求都需要认证
.and()
.csrf().disable(); //关闭csrf防御机制
}
自定义用户实现UserDetailsService接口重写loadUserByUsername方法
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return new User(username, "123456", true, true,
true, true, new ArrayList());
}
这里的User是security自己的,写死了密码为123456,正常应该根据用户名从数据库查出用户信息。当然密码也不可
能为明文,这里推荐使用BCryptPasswordEncoder加密,因为其每次加密都会生成随机盐加入字符串中。需要在之前申
明的config类添加即可
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
这四个boolean值分别表示是否可用、是否账户没有过期、是否密码没有过期、是否账户没有被锁定。构造函数
最后一个参数为该用户用哪些接口权限,同样也从数据库查,但是这个权限只会在登录时初始化一次,若我登录后修
改权限,则无法同步,后续介绍解决办法。
接下来就是两个登录的自定义登录处理,成功的实现AuthenticationSuccessHandler接口,失败的则实现
AuthenticationFailureHandler接口
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException{
response.setContentType("application/json;UTF-8");
response.getWriter().println("{\"success\":true,\"result\":\"" + "登录成功" + "\"}");
}
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setContentType("application/json;UTF-8");
response.getWriter().println("{\"error\":true,\"message\":\"登录名或者密码错误\"}");
}
注意成功和失败最后的方法参数是不同的,一个是成功的认证信息,另一个失败抛出的认证异常json串的key可以
根据自身登录页面ajax处理结果的属性来定,必须保持一致。
页面代码就不复制了,做一个简单html就行。
4. Помни меня
remember me顾名思义就是记住我,在登录成功认证以后服务端会发送cookie给浏览器,同时把用户名、base64加密随
机序列、生成token存入persistent_logins表中。当我下次再访问时,会读取cookie中的token与数据库中的token做
对比,若一直能表示认证通过,会重新生成新的token存入数据库中,序列户不变,再重新生成新的cookie发给浏览器。
下面看具体源码:
在第二部分说了登录认证成功后会调用rememberMeServices的loginSuccess方法,这是最后调用的方法,我们看
到了它构造了一个rememberMeToken,将其存入数据库,并发送cookie。
当下次登录,会请求到RememberMeAuthenticationFilter,它调用rememberMeServices接口的autoLogin方法,
去对比cookie中的token与数据库中的token,又判断了token是否过期,验证通过会根据token中的用户名调用
userDetailsService的loadUserByUsername方法获取用户信息。最后根据用户信息构造
一个RememberMeAuthenticationToken认证成功。
4. Осознание «Помни меня»
remember me实现较为简单,只需要在申明的config类中添加
@Autowired
private DataSource dataSource;
@Autowired
private UserDetailsService userDetailsService;
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
//tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
在configure方法里加
.rememberMe()
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(3600) //有效时间
.userDetailsService(userDetailsService)
5. Кодовый адрес
https://github.com/qumaoming/spring-security