一、默认情况
安装 spring security啥也不干,就可以拦截所有请求,并且在新版本开启了表单登陆
二、移除Spring security 所有拦截
@Override
public void configure(WebSecurity web) throws Exception {
//放开所有拦截
web.ignoring().antMatchers("/**");
}
三、需求是 自定义用户认证逻辑
我们做校验肯定不能用自带的认证
-
处理用户信息的获取
package org.springframework.security.core.userdetails; public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }
实现上面的接口就可以获取用户信息
-
用户校验
用户校验是根据用户名来的
-
处理密码的加密解密
密码加密解密可以配置,也可以自定义
下面我就自己配置了一个encoder
步骤是自己现实一个接口就可以了,不用再添加额外的配置,不需要把这个类配置到配置文件
package com.facebook.rbac.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
/**
* @author allen
*/
@Component
@Slf4j
public class MyUserDetailService implements UserDetailsService {
@Bean
private PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("用户名是 {}", username);
String password = passwordEncoder().encode("123456");
/**
user是自带的用户类,包含了用户的
* private String password;
private final String username;
private final Set<GrantedAuthority> authorities;
private final boolean accountNonExpired;
private final boolean accountNonLocked;
private final boolean credentialsNonExpired;
private final boolean enabled;
*
* */
User user = new User(
username,
password,
true, true, true, true,
AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
// System.out.println(user.toString());
return user;
}
}
四、 个性化用户认证流程
我们不想用它自动生成的登录页面,
登陆成功之后默认行为是跳转到未登录时候的页面,我们想要定制登陆成功之后的行为,比如发通知,发积分。。。返回用户信息
登录失败之后记录到日志该怎么办?
- 自定义登陆页面
配置文件中写入自定义登录页的地址
@Override
public void configure(HttpSecurity http) throws Exception {
http
.formLogin() //开启form表单校验
// .loginPage("/authentication/require")
.loginPage("/login.html")
// .permitAll()
// .loginProcessingUrl("/authentication/form")
.and()
.authorizeRequests()
// * 路径是/的时候 都通过
// *//*
.antMatchers("/", "/authentication/require", "/login", "/login.html","/swagger-ui.html").permitAll()
.anyRequest().authenticated() // 除此之外所有的请求都需要认证
.and()
.logout().permitAll() //登出的时候就不拦截了 都通过
.and()
.csrf().disable(); //关闭跨站请求拦截
}
编写一个controller
package com.facebook.rbac.api;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.savedrequest.SavedRequest;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* @author allen
*/
@RestController
@Slf4j
public class BrowserSecurityController {
private RequestCache requestCache = new HttpSessionRequestCache();
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
/**
* 需要身份认证的时候跳转到这里
*
* @param request
* @param response
* @return
*/
@RequestMapping("/authentication/require")
public ResponseEntity requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {
SavedRequest save = requestCache.getRequest(request, response);
log.info("请求的缓存是 {}", save);
if (save != null) {
String redirectUrl = save.getRedirectUrl();
log.info("引发跳转的请求是:{}", redirectUrl);
if (StringUtils.endsWithIgnoreCase(redirectUrl, ".html")) {
redirectStrategy.sendRedirect(request, response, "/login.html");
}
}
HashMap<Object, Object> objectObjectHashMap = new HashMap<>();
objectObjectHashMap.put("content", "需要认证");
ResponseEntity responseEntity = new ResponseEntity<>(objectObjectHashMap, HttpStatus.UNAUTHORIZED);
return responseEntity;
}
}
配置该controller
package com.facebook.rbac.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 2. 定义安全拦截机制
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http
.formLogin() //开启form表单校验
.loginPage("/authentication/require")
// .loginPage("/login.html")
// .permitAll()
// .loginProcessingUrl("/authentication/form")
.and()
.authorizeRequests()
// * 路径是/的时候 都通过
// *//*
.antMatchers("/", "/authentication/require", "/login", "/login.html","/swagger-ui.html").permitAll()
.anyRequest().authenticated() // 除此之外所有的请求都需要认证
.and()
.logout().permitAll() //登出的时候就不拦截了 都通过
.and()
.csrf().disable(); //关闭跨站请求拦截
}
/**
* @param web 放开静态资源拦截
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
//解决静态资源被拦截的问题
web.ignoring().antMatchers("/css/**","/vendors/**","/js/**","/webjars/**");
// web.ignoring().antMatchers("/**");
}
}
自定义登陆成功失败时候的响应
package com.facebook.rbac.authentication;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author allen
*/
@Component
@Slf4j
public class MySuccessAuthenticationHandler implements AuthenticationSuccessHandler {
@Resource
private ObjectMapper objectMapper;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
log.info("authentication {}", authentication);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(authentication));
}
}
package com.facebook.rbac.authentication;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author allen
*/
@Component
@Slf4j
public class MyFailAuthenticationHandler implements AuthenticationFailureHandler {
@Resource
private ObjectMapper objectMapper;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
log.info("exception {}", exception.getMessage());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(exception));
}
}
package com.facebook.rbac.config;
import com.facebook.rbac.authentication.MyFailAuthenticationHandler;
import com.facebook.rbac.authentication.MySuccessAuthenticationHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import javax.annotation.Resource;
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private MySuccessAuthenticationHandler successHandler;
@Resource
private MyFailAuthenticationHandler failAuthenticationHandler;
/**
* 2. 定义安全拦截机制
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http
.formLogin() //开启form表单校验
// .loginPage("/authentication/require")
// .loginPage("/login.html")
// .permitAll()
// .loginProcessingUrl("/authentication/form")
.successHandler(successHandler)
.failureHandler(failAuthenticationHandler)
.and()
.authorizeRequests()
// * 路径是/的时候 都通过
// *//*
.antMatchers("/", "/authentication/require", "/login", "/login.html", "/swagger-ui.html").permitAll()
.anyRequest().authenticated() // 除此之外所有的请求都需要认证
.and()
.logout().permitAll() //登出的时候就不拦截了 都通过
.and()
.csrf().disable(); //关闭跨站请求拦截
}
/**
* @param web 放开静态资源拦截
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
//解决静态资源被拦截的问题
web.ignoring().antMatchers("/css/**", "/vendors/**", "/js/**", "/webjars/**");
// web.ignoring().antMatchers("/**");
}
}
五、系统配置封装
定义层级配置关系:
#是否激活 swagger true or false
swagger.enable=false
facebook.security.browser.loginPage=/login.html
package com.facebook.rbac.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author allen
*/
@ConfigurationProperties(prefix = "facebook.security")
@Data
public class SecurityProperties {
private BrowserProperties browser = new BrowserProperties();
}
package com.facebook.rbac.config;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
/**
* @author allen
*/
@Data
public class BrowserProperties {
private String loginPage;
}
package com.facebook.rbac.config;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* @author allen
*/
@Configuration
@EnableConfigurationProperties(SecurityProperties.class)
public class SecurityCoreConfig {
}
使用的时候:
package com.facebook.rbac.api;
import com.facebook.rbac.config.SecurityProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.savedrequest.SavedRequest;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* @author allen
*/
@RestController
@Slf4j
public class BrowserSecurityController {
private RequestCache requestCache = new HttpSessionRequestCache();
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Resource
private SecurityProperties securityProperties;
/**
* 需要身份认证的时候跳转到这里
*
* @param request
* @param response
* @return
*/
@RequestMapping("/authentication/require")
public ResponseEntity requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {
SavedRequest save = requestCache.getRequest(request, response);
log.info("请求的缓存是 {}", save);
if (save != null) {
String redirectUrl = save.getRedirectUrl();
log.info("引发跳转的请求是:{}", redirectUrl);
if (StringUtils.endsWithIgnoreCase(redirectUrl, ".html")) {
redirectStrategy.sendRedirect(request, response, securityProperties.getBrowser().getLoginPage());
}
}
HashMap<Object, Object> objectObjectHashMap = new HashMap<>();
objectObjectHashMap.put("content", "需要认证");
ResponseEntity responseEntity = new ResponseEntity<>(objectObjectHashMap, HttpStatus.UNAUTHORIZED);
return responseEntity;
}
}
六、认证流程详解
认证处理流程
认证结果如何在多个请求之间共享
获取认证用户信息
1、校验流程图
2、源码分析
AbstractAuthenticationProcessingFilter 抽象类
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}
Authentication authResult;
try {
authResult = attemptAuthentication(request, response);
if (authResult == null) {
// return immediately as subclass has indicated that it hasn't completed
// authentication
return;
}
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
// Authentication failed
unsuccessfulAuthentication(request, response, failed);
return;
}
// Authentication success
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
successfulAuthentication(request, response, chain, authResult);
}
调用 requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。如果需要验证,则会调用 attemptAuthentication(HttpServletRequest, HttpServletResponse) 方法,有三种结果:
-
返回一个 Authentication 对象。配置的 SessionAuthenticationStrategy` 将被调用,然后 然后调用 successfulAuthentication(HttpServletRequest,HttpServletResponse,FilterChain,Authentication) 方法。
-
验证时发生 AuthenticationException。unsuccessfulAuthentication(HttpServletRequest, HttpServletResponse, AuthenticationException) 方法将被调用。
-
返回Null,表示身份验证不完整。假设子类做了一些必要的工作(如重定向)来继续处理验证,方法将立即返回。假设后一个请求将被这种方法接收,其中返回的Authentication对象不为空。
UsernamePasswordAuthenticationFilter(AbstractAuthenticationProcessingFilter的子类)**
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);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
attemptAuthentication () 方法将 request 中的 username 和 password 生成 UsernamePasswordAuthenticationToken 对象,用于 AuthenticationManager 的验证(即 this.getAuthenticationManager().authenticate(authRequest) )。
默认情况下注入 Spring 容器的 AuthenticationManager 是 ProviderManager。
ProviderManager(AuthenticationManager的实现类)
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
boolean debug = logger.isDebugEnabled();
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;
}
}
catch (AccountStatusException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
}
catch (InternalAuthenticationServiceException e) {
prepareException(e, authentication);
throw e;
}
catch (AuthenticationException e) {
lastException = e;
}
}
if (result == null && parent != null) {
// Allow the parent to try.
try {
result = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException e) {
lastException = e;
}
}
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
eventPublisher.publishAuthenticationSuccess(result);
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
prepareException(lastException, authentication);
throw lastException;
}
尝试验证 Authentication 对象。AuthenticationProvider 列表将被连续尝试,直到 AuthenticationProvider 表示它能够认证传递的过来的Authentication 对象。然后将使用该 AuthenticationProvider 尝试身份验证。如果有多个 AuthenticationProvider 支持验证传递过来的Authentication 对象,那么由第一个来确定结果,覆盖早期支持AuthenticationProviders 所引发的任何可能的AuthenticationException。 成功验证后,将不会尝试后续的AuthenticationProvider。如果最后所有的 AuthenticationProviders 都没有成功验证 Authentication 对象,将抛出 AuthenticationException。从代码中不难看出,由 provider 来验证 authentication, 核心点方法是:
Authentication result = provider.authenticate(authentication);
此处的 provider 是 AbstractUserDetailsAuthenticationProvider,AbstractUserDetailsAuthenticationProvider 是AuthenticationProvider的实现,看看它的 authenticate(authentication) 方法:
// 验证 authentication
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
// Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found");
if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
else {
throw notFound;
}
}
Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
}
try {
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
if (cacheWasUsed) {
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
else {
throw exception;
}
}
postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return createSuccessAuthentication(principalToReturn, authentication, user);
}
AbstractUserDetailsAuthenticationProvider 内置了缓存机制,从缓存中获取不到的 UserDetails 信息的话,就调用如下方法获取用户信息,然后和 用户传来的信息进行对比来判断是否验证成功。
// 获取用户信息
UserDetails user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
retrieveUser() 方法在 DaoAuthenticationProvider 中实现,DaoAuthenticationProvider 是 AbstractUserDetailsAuthenticationProvider的子类。具体实现如下:
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
UserDetails loadedUser;
try {
loadedUser = this.getUserDetailsService().loadUserByUsername(username);
}
catch (UsernameNotFoundException notFound) {
if (authentication.getCredentials() != null) {
String presentedPassword = authentication.getCredentials().toString();
passwordEncoder.isPasswordValid(userNotFoundEncodedPassword,
presentedPassword, null);
}
throw notFound;
}
catch (Exception repositoryProblem) {
throw new InternalAuthenticationServiceException(
repositoryProblem.getMessage(), repositoryProblem);
}
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
可以看到此处的返回对象 userDetails 是由 UserDetailsService 的 loadUserByUsername(username) 来获取的。
需要获取登陆后的用户信息
@GetMapping("/user")//全部信息
public Authentication userDetails(Authentication authentication) {
return authentication;
}
@GetMapping("/userSimplify") // 只有用户信息
public UserDetails userSimplify(@AuthenticationPrincipal UserDetails user) {
return user;
}