SpringSecurity
github:https://github.com/Fly0708/securityDemo.git
一、框架的基础使用
当pom文件中加入Spring Security依赖时,默认开启权限认证
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
自定义SecurityConfig配置类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SuccessHandler successHandler;
@Autowired
private FailureHandler failureHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//basic和表单两种方式,formLogin的拦截器UsernamePasswordAuthenticationFilter
.formLogin()
.loginPage("/login")
//自定义登录url(覆盖AbstractAuthenticationProcessingFilter中的RequestMatcher),post方式,
.loginProcessingUrl("/login")
//覆盖默认成功处理器,默认实现跳转上一url,先返回登录用户信息json
.successHandler(successHandler)
//覆盖默认失败处理器,默认实现跳转登录页面loginPage,先返回失败信息
.failureHandler(failureHandler)
.and()
//对授权的路径进行配置,
.authorizeRequests()
.antMatchers("/login.html","/login").permitAll()
.anyRequest().authenticated()
.and()
.csrf().disable();
}
//spring security内置的加密器,加密中的字符串中有salt,每次加密的salt均不一样,所以对同一内容加密后的结果是不同的。
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
SuccessHandler.java
@Component
public class SuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
ObjectMapper objectMapper = new ObjectMapper();
response.getWriter().write(objectMapper.writeValueAsString(authentication));
}
}
FailureHandler.java
@Component
public class FailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(500);
ObjectMapper objectMapper = new ObjectMapper();
String content = objectMapper.writeValueAsString(exception.getMessage());
response.getWriter().write(content);
}
}
UserService.java
//为方便,模拟从数据库查询用户信息
@Service
public class UserService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Set<GrantedAuthority> authorities = new HashSet<>();
authorities.add(new SimpleGrantedAuthority("admin"));
return new User("fly",passwordEncoder.encode("123"),authorities);
}
}
效果:
访问http://localhost:8080/test
,返回{"result":"0","message":"请登录","data":null}
访问http://localhost:8080/login.html
,输入错误用户名或密码,返回{"result":"0","message":"用户名或密码错误","data":null}
//具体错误信息可通过覆盖DaoAuthenticationProvider
或者在UserDetailService
中添加逻辑代码实现。输入正确用户名 密码,返回
{
"authorities": [{
"authority": "admin"
}],
"details": {
"remoteAddress": "0:0:0:0:0:0:0:1",
"sessionId": "21385DFFE1DC1DBEE43CDB6F8FD00444"
},
"authenticated": true,
"principal": {
"password": null,
"username": "fly",
"authorities": [{
"authority": "admin"
}],
"accountNonExpired": true,
"accountNonLocked": true,
"credentialsNonExpired": true,
"enabled": true
},
"credentials": null,
"name": "fly"
}
自定义格式在SuccessHandler中实现。
过程分析
一张经典的SpringSecurity执行图
SpringSecurity的执行是依赖一套FilterChain来实现的
SecurityContextPersistenceFilter
是第二个Filter,通过断点方式来看这条Filter Chain
比较重要的几个Filter:
SecurityContextPersistenceFilter:在每次请求处理之前将该请求相关的安全上下文信息加载到SecurityContextHolder中,用于FilterSecurityInterceptor判断是否已经认证,SecurityContextHolder中存储。
ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();SecurityContextHolder中的SecurityContext是线程级变量
UsernamePasswordAuthenticationFilter:拦截请求(看其构造函数中配置)。这里采用了模板的方式,具体拦截过程在AbstractAuthenticationProcessingFilter中的doFilter方法。
FilterSecurityInterceptor:根据配置,即那些请求需要permitAll ,哪些请求需要authenticated。
主要的验证逻辑是在UsernamePasswordAuthenticationFilter
中实现的
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter#UsernamePasswordAuthenticationFilter(构造函数)
public UsernamePasswordAuthenticationFilter() {
//这个类继承了一个抽象类,这里的构造函数设置了父类的requiresAuthenticationRequestMatcher属性(在判断是否拦截请求的时候用到)
super(new AntPathRequestMatcher("/login", "POST"));
}
org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter#doFilter
...
//用上面的requiresAuthenticationRequestMatcher进行匹配判断,即拦截路径/login POST请求
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
...
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{
...
}
...
//调用登录成功处理器
successfulAuthentication(request, response, chain, authResult);
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter#attemptAuthentication
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();
//封装用于验证的token
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property sessionID,封装请求的一些信息
setDetails(request, authRequest);
//得到AuthenticationManager对象(全局唯一)的实现类ProviderManager进行登录验证
return this.getAuthenticationManager().authenticate(authRequest);
}
org.springframework.security.authentication.ProviderManager#authenticate
...
//通过循环所注册的provider,通过provider的support(token)方法查找能处理该类型token的provider
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
}
try {
//支持UsernamePasswordAuthenticationToken的是DaoAuthenticationProvider
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider#authenticate
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user),
org.springframework.security.authentication.dao.DaoAuthenticationProvider#retrieveUser
//利用userDetailService查找用户,security自动配置
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
org.springframework.security.authentication.dao.DaoAuthenticationProvider#additionalAuthenticationChecks
...
//这个方法对密码进行了校验
String presentedPassword = authentication.getCredentials().toString();
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
整体的流程是这样的,
一张经典的图
所以可以通过添加自定义filter provider token可以实现自定义登录流程,