一、Spring Security基本原理
如上图:
1. SecurityContextPersistenceFilter
一个请求和相应的过程为一个线程。
收到请求时,判断session中是否有SecurityContext,如果有则放到当前线程中。请求响应时经过该过滤器,检查当前线程中是否有SecurityContetx,如果有则将其放入session中,这样不同的请求就可以在同一个session中得到相同的信息。SecurityContetx中存放的是当前用户的信息。
2. 认证过滤器
UsernamePasswordAuthenticationFilter:处理表单登录的过滤器;
BasicAuthenticationFilter:处理http basic登录请求的过滤器;
- 发送登录请求时,检查登录请求中是否带有该过滤器所需的信息。
- 如果请求中包含用户名和密码,则会进入表单认证的过滤器,如果不包含用户名和密码,则会进入下一个认证过滤器,即Http Basic登录认证的过滤器,如果请求中包含basic开头的Authentication的信息,则做base64解码取出用户名和密码,如果没有继续到下一个认证过滤器中。
- 任一认证过滤器认证成功,会在请求中添加标记,表示该用户已经认证成功。
3. FilterSecurityInterceptor
经过认证过滤器后来到最后一个过滤器FilterSecurityInterceprot,该过滤器决定当前请求能否访问真正的服务,访问规则在SecurityConfig中配置,如果请求不满足规定的条件,会根据不能访问的原因抛出相应的异常。
```
http.csrf().disable()// 关闭csrf验证
// 对请求进行认证
.authorizeRequests()
// 所有请求需要身份认证
.anyRequest().authenticated()
//表单登录
.and().formLogin()
```
4. ExceptionTranslationFilter
捕获FilterSecurityInterceptor抛出的异常,根据捕获的异常类型做相应的处理。eg:因为没有登录而不能访问时,将页面定位到登录页面。
二、认证流程
如果是表单登录,进入UsernamePasswordAuthenticationFilter,处理代码:
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException( "Authentication method not supported: " + request.getMethod()); } String username = obtainUsername(request); String password = obtainPassword(request); ...... //通过用户名和密码构建UsernamePasswordAuthenticationToken对象,其中设置认证标记false,权限列表为空 UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); // 将当前请求的信息包括IP、session等放进UsernamePasswordAuthenticationToken中 setDetails(request, authRequest); //认证 return this.getAuthenticationManager().authenticate(authRequest); }
AuthenticationManager: 用来管理AuthenticationProvider。收集所有的AuthenticationProvider,收到请求时,循环它们,判断当前AuthenticationProvider是否支持本次的登录方式,最终找到真正处理用户认证逻辑的AuthenticationProvider。
ProviderManager中authenticate()的主要代码://本次登录方式 Class<? extends Authentication> toTest = authentication.getClass(); ...... ...... for (AuthenticationProvider provider : getProviders()) { if (!provider.supports(toTest)) { continue; } if (debug) { logger.debug("Authentication attempt using " + provider.getClass().getName()); } try { result = provider.authenticate(authentication); if (result != null) { copyDetails(authentication, result); break; } } ....... }
AuthenticationProvider:根据用户名获取UserDetails对象,对当前用户进行预检查、密码的验证和后检查,如果检查通过,将用户的权限等信息封装形成Authentication对象。
实现AuthenticationProvider,执行真正认证逻辑的类DaoAuthenticationProvider。
主要认证代码:
public Authentication authenticate(Authentication authentication) throws AuthenticationException { String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName(); boolean cacheWasUsed = true; UserDetails user = this.userCache.getUserFromCache(username); if (user == null) { cacheWasUsed = false; try { //根据用户名获取UserDetails对象 user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); } ....... } try { //用户信息的预检查 (账户是否可用、是否存在、是否过期) preAuthenticationChecks.check(user); //验证密码 additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } ....... //密码验证成功后,检查用户信息是否已过期(密码等) postAuthenticationChecks.check(user); //全部验证成功,返回UsernamePasswordAuthenticationToken对象,包括用户的权限等信息,认证标记为true return createSuccessAuthentication(principalToReturn, authentication, user); }
以上认证全部通过后,在认证过滤器中调用successfulAuthentication()方法,在该方法中将返回的UsernamePasswordAuthenticationToken放入SecurityContext、SecurityContextHolder中,在经过SecurityContextPersistenceFilter过滤器时将SecurityContext放入session中,通过SecurityContextPersistenceFilter可以使认证结果在多个请求中共享。
获取认证的用户信息:
SpringMVC会自动在SecurityContext中查找Authentication类型的对象。
@GetMapping(value = "getUser") public Object getUser(Authentication authentication){ return authentication; }
返回结果:
三、 用户认证的处理
1. 用户信息的获取逻辑
用户信息的获取逻辑封装在UserDetailsService接口中,接口中定义的方法:
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
作用:根据登录时输入的用户名去数据库中读取用户信息,并封装到UserDetails的实现类User
中。如果没有找到对应的用户信息,抛出UsernameNotFoundException异常。
2. 用户校验逻辑
UserDetails接口中声明了关于用户校验的属性,包括用户是否过期、密码是否过期、账户是否锁定等,形成User
对象时可以指定这些属性的值。
3. 密码加解密
密码加解密通过实现PasswordEncoder接口来完成。该接口中包含两个方法
- 加密方法:用户注册时调用该方法,将加密后的密码存进数据库中。
String encode(CharSequence rawPassword);
- 校验方法:对数据库中保存的密码和页面上填写的密码进行校验。Spring Security在用户登陆,生成
User
对象后,将User中的密码和登陆请求中的密码进行比对,如果比对不成功,该方法返回false,Spring Security抛出异常。
boolean matches(CharSequence rawPassword, String encodedPassword);
四、 自定义登陆功能
- 如下代码:
// 关闭csrf验证
http.csrf().disable()
//对请求授权
.authorizeRequests()
// 登陆请求允许全部访问,如果没有该配置会形成死循环,提示重定向次数过多
.antMatchers("/signIn.html").permitAll()
// 所有请求需要身份认证
.anyRequest().authenticated()
//设置表单登录,登陆页面为/signIn.html
.and().formLogin().loginPage("/signIn.html")
//设置自定义的处理登陆的post请求
.loginProcessingUrl("/authentication/form");
五、添加记住我功能
记住我的基本原理
RememberMeAuthenticationFilter在过滤器链中在认证过滤器之后。
登陆认证成功之后,执行认证过滤器中的successfulAuthentication()方法,在该方法中执行
rememberMeServices.loginSuccess(request, response, authResult);
RememberMeServices的实现类PersistentTokenBasedRememberMeServices中该方法的具体实现为:
protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) { String username = successfulAuthentication.getName(); PersistentRememberMeToken persistentToken = new PersistentRememberMeToken( username, generateSeriesData(), generateTokenData(), new Date()); try { //生成token,并保存在数据库中 tokenRepository.createNewToken(persistentToken); //将token保存在cookie中 addCookie(persistentToken, request, response); } catch (Exception e) { logger.error("Failed to save persistent token ", e); } }
登陆成功后,退出此次登陆,再次发送请求时经过认证过滤器后,会被RememberMeAuthenticationFilter拦截,调用autoLogin()方法,在该方法中通过tokenRepository根据cookie中的token获取数据库中存放的token信息,验证通过后,根据token中的用户名获取用户信息并返回。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { //在该过滤器之前没有认证过,调用rememberMeServices的自动登录方法 if (SecurityContextHolder.getContext().getAuthentication() == null) { Authentication rememberMeAuth = rememberMeServices.autoLogin(request, response); if (rememberMeAuth != null) { try { rememberMeAuth = authenticationManager.authenticate(rememberMeAuth); SecurityContextHolder.getContext().setAuthentication(rememberMeAuth); onSuccessfulAuthentication(request, response, rememberMeAuth); } } ...... } else { ...... chain.doFilter(request, response); } }
TokenRepository的作用:将token写入数据库中、用户再次访问请求时在数据库中查找当前token对应的用户名。
系统实现
(1)统一实现
配置TokenRepository
设置token的过期时间,以秒为单位
TokenRepository获取到用户名之后,通过myUserDetailsService获取用户信息,完成再一次的请求操作.rememberMe() .tokenRepository(persistentTokenRepository()) .tokenValiditySeconds(3600) .userDetailsService(myUserDetailsService)
(2) TokenRepository的配置
@Bean public PersistentTokenRepository persistentTokenRepository(){ JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl(); //设置数据源 tokenRepository.setDataSource(dataSource); //系统启动时自动创建表 tokenRepository.setCreateTableOnStartup(true); return tokenRepository; }
(3) 在页面上增加记住我的复选框,name属性固定为remember-me,如果要修改,同时修改SecurityCongfig中的rememberMeParameter属性的值。
<tr> <td colspan='2'><input name="remember-me" type="checkbox" value="true" />记住我</td> </tr>
上述配置完成以后,启动服务,在对应的数据库中会自动创建表
persistent_logins
,在登陆页面选中‘记住我‘登陆后会向该表中写入一条数据,包括用户名、token等信息,同时会将token保存在Cookie中。
退出本次登陆,再次访问受保护的请求时,TokenRepository会根据Cookie中保存的Token查询persistent_logins中对应的记录,获取到用户名,根据用户名获取该用户的权限等信息。