SpringSecurity初体验
第一步:创建SpringBoot项目
第二步:导入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.shao</groupId>
<artifactId>securitydemo01</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>securitydemo01</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
第三步:创建测试的Controller
package com.shao.securitydemo01.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("hello")
public String Hello(){
return "hello security !";
}
}
第四步:网页测试及结果:
网页输入地址:http://localhost:8811/test/hello
我们会发现会出现一个security默认的登陆框,这说明Security已经起作用了.
Security基本原理
1.Security底层本质是一个一个的过滤器组成的过滤器链,在springboot启动时会自动加载这些过滤器,接下来看几个简单的过滤器:
FilterSecurityInterceptor:这是一个方法过滤器,位于过滤器链的最底层(最后一个过滤器):
package org.springframework.security.web.access.intercept;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.springframework.security.access.SecurityMetadataSource;
import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
import org.springframework.security.access.intercept.InterceptorStatusToken;
import org.springframework.security.web.FilterInvocation;
//继承了Filter,说明是一个过滤器
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
private static final String FILTER_APPLIED = "__spring_security_filterSecurityInterceptor_filterApplied";
private FilterInvocationSecurityMetadataSource securityMetadataSource;
private boolean observeOncePerRequest = true;
public FilterSecurityInterceptor() {
}
public void init(FilterConfig arg0) {
}
public void destroy() {
}
//dofilter方法中执行真正过滤的方法--invoke方法
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
this.invoke(new FilterInvocation(request, response, chain));
}
public FilterInvocationSecurityMetadataSource getSecurityMetadataSource() {
return this.securityMetadataSource;
}
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.securityMetadataSource;
}
public void setSecurityMetadataSource(FilterInvocationSecurityMetadataSource newSource) {
this.securityMetadataSource = newSource;
}
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}
public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
if (this.isApplied(filterInvocation) && this.observeOncePerRequest) {
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
} else {
if (filterInvocation.getRequest() != null && this.observeOncePerRequest) {
filterInvocation.getRequest().setAttribute("__spring_security_filterSecurityInterceptor_filterApplied", Boolean.TRUE);
}
//判断之前的过滤器是否通过
InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
//执行本身的过滤器方法
try {
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
} finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, (Object)null);
}
}
private boolean isApplied(FilterInvocation filterInvocation) {
return filterInvocation.getRequest() != null && filterInvocation.getRequest().getAttribute("__spring_security_filterSecurityInterceptor_filterApplied") != null;
}
public boolean isObserveOncePerRequest() {
return this.observeOncePerRequest;
}
public void setObserveOncePerRequest(boolean observeOncePerRequest) {
this.observeOncePerRequest = observeOncePerRequest;
}
}
ExceptionTranslationFiler:这是一个异常过滤器,用来处理在认证和授权过程中抛出的异常:
package org.springframework.security.web.access;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.core.log.LogMessage;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.util.ThrowableAnalyzer;
import org.springframework.util.Assert;
import org.springframework.web.filter.GenericFilterBean;
public class ExceptionTranslationFilter extends GenericFilterBean {
private AccessDeniedHandler accessDeniedHandler;
private AuthenticationEntryPoint authenticationEntryPoint;
private AuthenticationTrustResolver authenticationTrustResolver;
private ThrowableAnalyzer throwableAnalyzer;
private RequestCache requestCache;
private final MessageSourceAccessor messages;
public ExceptionTranslationFilter(AuthenticationEntryPoint authenticationEntryPoint) {
this(authenticationEntryPoint, new HttpSessionRequestCache());
}
public ExceptionTranslationFilter(AuthenticationEntryPoint authenticationEntryPoint, RequestCache requestCache) {
this.accessDeniedHandler = new AccessDeniedHandlerImpl();
this.authenticationTrustResolver = new AuthenticationTrustResolverImpl();
this.throwableAnalyzer = new ExceptionTranslationFilter.DefaultThrowableAnalyzer();
this.requestCache = new HttpSessionRequestCache();
this.messages = SpringSecurityMessageSource.getAccessor();
Assert.notNull(authenticationEntryPoint, "authenticationEntryPoint cannot be null");
Assert.notNull(requestCache, "requestCache cannot be null");
this.authenticationEntryPoint = authenticationEntryPoint;
this.requestCache = requestCache;
}
public void afterPropertiesSet() {
Assert.notNull(this.authenticationEntryPoint, "authenticationEntryPoint must be specified");
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
this.doFilter((HttpServletRequest)request, (HttpServletResponse)response, chain);
}
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
try {
chain.doFilter(request, response);
} catch (IOException var7) {
throw var7;
} catch (Exception var8) {
Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(var8);
RuntimeException securityException = (AuthenticationException)this.throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain);
if (securityException == null) {
securityException = (AccessDeniedException)this.throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
}
if (securityException == null) {
this.rethrow(var8);
}
if (response.isCommitted()) {
throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", var8);
}
this.handleSpringSecurityException(request, response, chain, (RuntimeException)securityException);
}
}
private void rethrow(Exception ex) throws ServletException {
if (ex instanceof ServletException) {
throw (ServletException)ex;
} else if (ex instanceof RuntimeException) {
throw (RuntimeException)ex;
} else {
throw new RuntimeException(ex);
}
}
public AuthenticationEntryPoint getAuthenticationEntryPoint() {
return this.authenticationEntryPoint;
}
protected AuthenticationTrustResolver getAuthenticationTrustResolver() {
return this.authenticationTrustResolver;
}
private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, RuntimeException exception) throws IOException, ServletException {
if (exception instanceof AuthenticationException) {
this.handleAuthenticationException(request, response, chain, (AuthenticationException)exception);
} else if (exception instanceof AccessDeniedException) {
this.handleAccessDeniedException(request, response, chain, (AccessDeniedException)exception);
}
}
private void handleAuthenticationException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, AuthenticationException exception) throws ServletException, IOException {
this.logger.trace("Sending to authentication entry point since authentication failed", exception);
this.sendStartAuthentication(request, response, chain, exception);
}
private void handleAccessDeniedException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, AccessDeniedException exception) throws ServletException, IOException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
boolean isAnonymous = this.authenticationTrustResolver.isAnonymous(authentication);
if (!isAnonymous && !this.authenticationTrustResolver.isRememberMe(authentication)) {
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("Sending %s to access denied handler since access is denied", authentication), exception);
}
this.accessDeniedHandler.handle(request, response, exception);
} else {
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("Sending %s to authentication entry point since access is denied", authentication), exception);
}
this.sendStartAuthentication(request, response, chain, new InsufficientAuthenticationException(this.messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication", "Full authentication is required to access this resource")));
}
}
protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, AuthenticationException reason) throws ServletException, IOException {
SecurityContextHolder.getContext().setAuthentication((Authentication)null);
this.requestCache.saveRequest(request, response);
this.authenticationEntryPoint.commence(request, response, reason);
}
public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) {
Assert.notNull(accessDeniedHandler, "AccessDeniedHandler required");
this.accessDeniedHandler = accessDeniedHandler;
}
public void setAuthenticationTrustResolver(AuthenticationTrustResolver authenticationTrustResolver) {
Assert.notNull(authenticationTrustResolver, "authenticationTrustResolver must not be null");
this.authenticationTrustResolver = authenticationTrustResolver;
}
public void setThrowableAnalyzer(ThrowableAnalyzer throwableAnalyzer) {
Assert.notNull(throwableAnalyzer, "throwableAnalyzer must not be null");
this.throwableAnalyzer = throwableAnalyzer;
}
private static final class DefaultThrowableAnalyzer extends ThrowableAnalyzer {
private DefaultThrowableAnalyzer() {
}
protected void initExtractorMap() {
super.initExtractorMap();
this.registerExtractor(ServletException.class, (throwable) -> {
ThrowableAnalyzer.verifyThrowableHierarchy(throwable, ServletException.class);
return ((ServletException)throwable).getRootCause();
});
}
}
}
UsernamePasswordAuthenticationFilter:对/login的Post请求做拦截,校验表单中的用户名和密码:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.springframework.security.web.authentication;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.Assert;
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login", "POST");
private String usernameParameter = "username";
private String passwordParameter = "password";
private boolean postOnly = true;
public UsernamePasswordAuthenticationFilter() {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
}
public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
}
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
String username = this.obtainUsername(request);
username = username != null ? username : "";
username = username.trim();
String password = this.obtainPassword(request);
password = password != null ? password : "";
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
@Nullable
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(this.passwordParameter);
}
@Nullable
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(this.usernameParameter);
}
protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
public void setUsernameParameter(String usernameParameter) {
Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
this.usernameParameter = usernameParameter;
}
public void setPasswordParameter(String passwordParameter) {
Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
this.passwordParameter = passwordParameter;
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getUsernameParameter() {
return this.usernameParameter;
}
public final String getPasswordParameter() {
return this.passwordParameter;
}
}
2.过滤器的加载过程.
因为我们开发过程中用的spring boot来集成Security,索引不需要我们自己配置,springboot已经帮助我们自动配置好了.如果不使用springboot那我们就要了解它的加载过程.
过滤器名字:DelegatFilterProxy
//在这个过滤器中的doFilter调用了initDelegate方法.在这个方法中获得一个Bean叫做FilterChainProxy
protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
String targetBeanName = this.getTargetBeanName();
Assert.state(targetBeanName != null, "No target bean name set");
Filter delegate = (Filter)wac.getBean(targetBeanName, Filter.class);
if (this.isTargetFilterLifecycle()) {
delegate.init(this.getFilterConfig());
}
return delegate;
}
//在FilterChainProxy这个过滤器中的doFilter中调用了doFilterInternal方法,这个方法中的getFilters方法来获取Security中的所有过滤器
private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
FirewalledRequest firewallRequest = this.firewall.getFirewalledRequest((HttpServletRequest)request);
HttpServletResponse firewallResponse = this.firewall.getFirewalledResponse((HttpServletResponse)response);
List<Filter> filters = this.getFilters((HttpServletRequest)firewallRequest);
if (filters != null && filters.size() != 0) {
if (logger.isDebugEnabled()) {
logger.debug(LogMessage.of(() -> {
return "Securing " + requestLine(firewallRequest);
}));
}
FilterChainProxy.VirtualFilterChain virtualFilterChain = new FilterChainProxy.VirtualFilterChain(firewallRequest, chain, filters);
virtualFilterChain.doFilter(firewallRequest, firewallResponse);
} else {
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.of(() -> {
return "No security for " + requestLine(firewallRequest);
}));
}
firewallRequest.reset();
chain.doFilter(firewallRequest, firewallResponse);
}
}
3.UserDatailsService和PasswordEncoder接口
3.1:UserDatailsService接口:查询数据库用户名和密码的过程
当我们什么都没有配置的时候,账号和密码都是由Security自动生成的.而在实际开发中账号和密码都是从数据库中查询出来的,所以我们要通过自定义逻辑控制认证逻辑.我们只需要继承UserDatailsServicejike接口即可.
在我们的开发过程中实际上是自己写一个过滤器来继承UsernamePasswordAuthenticationFilter这个过滤器,重写里面的attempAuthenicationFilter()这个方法进行用户名密码验证,重写它父类里面的successfulAuthentication()和unsuccessfulAuthentication()方法进行认证成功的操作和认证失败的操作.
在上面这个过滤器中我们的用户名和密码都是从数据库中进行查询的,这些操作就要写在UserDatailsService接口的实现类中.
具体的操作就是写一个类来继承UserDatailsService接口,接口中有一个方法loadUserByUsername()方法.通过用户名查询用户信息,最终创建Security中的User对象,用户名密码和权限等信息.
3.2.PasswordEncoder接口:
数据加密接口,用于返回User对象里面密码加密,Security只认识这种加密方式.