SpringSecurity笔记
SpringSecurity笔记
一. SpringSecurity使用
1. 依赖引入
org.springframework.boot:spring-boot-starter-security:2.2.9.RELEASE
io.jsonwebtoken:jjwt:0.12.6(可选)
2. SecurityConfig配置
2.1. 过滤链配置
Spring Security的一些安全配置,暂不关心
//过滤链配置
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http
.authorizeHttpRequests(authorize -> authorize//开启授权
.requestMatchers("/login","/api/register")//放行请求
.permitAll()//允许所有人访问
.anyRequest()//允许所有请求
.authenticated()//认证后访问自动授权
)
.formLogin(Customizer.withDefaults())//使用默认的登陆登出页面进行授权登陆
.rememberMe(Customizer.withDefaults());// 启用“记住我”功能的。允许用户在关闭浏览器后,仍然保持登录状态,直到他们主动注销或超出设定的过期时间。
http.csrf(csrf -> csrf.disable());//关闭csrf
return http.build();
}
关于放行路径的配置:
如果在 application.properities 中配置的有 server.servlet.context-path=/api 前缀的话,在放行路径中不需要写 /api。
如果 @RequestMapping(value = "/test/") 中写的是 /test/, 那么放行路径必须也写成 /test/, (/test)是不行的,反之亦然。
如果 @RequestMapping(value = "/test") 链接 /test 后面要加查询字符的话(/test?type=0),不要写成 @RequestMapping(value = "/test/")
2.2. 采用基于内存的用户认证
基于内存的方法,直接在SecurityConfig中注册为Bean即可
@Bean
public UserDetailsService userDetailsService() {
// 创建基于内存的用户信息管理器
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(
// 创建UserDetails对象,用于管理用户名、用户密码、用户角色、用户权限等内容
User.withDefaultPasswordEncoder().username("user").password("user123").roles("USER").build()
);
// 如果自己配置的有账号密码, 那么上面讲的 user 和 随机字符串 的默认密码就不能用了
return manager;
}
public UserDetailsService userDetailsService()方法中使用了InMemoryUserDetailsManager,显示创建并存储了用户信息,说明对用户信息的相关调用被封装在其内部。
追踪InMemoryUserDetailsManager源码可以得到其继承关系:
interface UserDetailsService
|
interface UserDetailsManager interface UserDetailsPasswordService
| |
class InMemoryUserDetailsManager
我们来关心它们的实现内容。
//UserDetailsService声明了loadUserByUsername方法,要求根据用户名返回UserDetails类型用户信息。
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
//UserDetailsPasswordService声明了updatePassword,要求更新UserDetails内的密码为newPassword,通常为系统操作
public interface UserDetailsPasswordService {
UserDetails updatePassword(UserDetails user, String newPassword);
}
//UserDetailsManager继承自UserDetailsService,在loadUserByUsername方法的基础上,声明了创建,更新,删除,更改密码,及判断存在这五个与用户信息操作相关的方法,此处的changePassword要求校验旧密码后更改为新密码/
public interface UserDetailsManager extends UserDetailsService {
void createUser(UserDetails user);
void updateUser(UserDetails user);
void deleteUser(String username);
void changePassword(String oldPassword, String newPassword);
boolean userExists(String username);
}
//此处省去了接口中方法的实现,留下了InMemoryUserDetailsManager单独实现的方法
//无参及有参构造方法,
//createUserDetails用于根据用户信息构建用户,返回封装好的User,
//最会传给createUser,再封装为MutableUser添加到内存用户管理器,保存于private final Map<String, MutableUserDetails> users = new HashMap();
//setSecurityContextHolderStrategy和setAuthenticationManager则是一些配置方法,此处暂不关心
public class InMemoryUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {
protected final Log logger = LogFactory.getLog(this.getClass());
private final Map<String, MutableUserDetails> users = new HashMap();
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder.getContextHolderStrategy();
private AuthenticationManager authenticationManager;
public InMemoryUserDetailsManager() {
}
public InMemoryUserDetailsManager(Collection<UserDetails> users) {
Iterator var2 = users.iterator();
while(var2.hasNext()) {
UserDetails user = (UserDetails)var2.next();
this.createUser(user);
}
}
public InMemoryUserDetailsManager(UserDetails... users) {
UserDetails[] var2 = users;
int var3 = users.length;
for(int var4 = 0; var4 < var3; ++var4) {
UserDetails user = var2[var4];
this.createUser(user);
}
}
public InMemoryUserDetailsManager(Properties users) {
Enumeration<?> names = users.propertyNames();
UserAttributeEditor editor = new UserAttributeEditor();
while(names.hasMoreElements()) {
String name = (String)names.nextElement();
editor.setAsText(users.getProperty(name));
UserAttribute attr = (UserAttribute)editor.getValue();
Assert.notNull(attr, () -> {
return "The entry with username '" + name + "' could not be converted to an UserDetails";
});
this.createUser(this.createUserDetails(name, attr));
}
}
private User createUserDetails(String name, UserAttribute attr) {
return new User(name, attr.getPassword(), attr.isEnabled(), true, true, true, attr.getAuthorities());
}
public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
this.securityContextHolderStrategy = securityContextHolderStrategy;
}
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
}
总结:
InMemoryUserDetailsManager实现了用户信息的封装,并用final Map<String, MutableUserDetails>保存用户于内存,继承一系列接口,实现了其对应的对用户信息的读取创建修改操作
2.3. 采用基于数据库的用户认证
分析过基于内存的方法可知,我们需要的是一个可以实现UserDetailsService的class,通过重写loadUserByUsername方法,即可自定义存储用户信息的位置
这是InMemoryUserDetailsManager中的loadUserByUsername实现,要求返回UserDetails,实际返回new User,即UserDetails的实现类。具体实现内容可自行查看源码
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDetails user = (UserDetails)this.users.get(username.toLowerCase());
if (user == null) {
throw new UsernameNotFoundException(username);
} else {
return new User(user.getUsername(), user.getPassword(), user.isEnabled(), user.isAccountNonExpired(), user.isCredentialsNonExpired(), user.isAccountNonLocked(), user.getAuthorities());
}
}
新写类UserDetailsServiceImpl实现UserDetailsService,重写loadUserByUsername
可以注意到最后return new UserDetailsImpl(user)与之前有所不同,后面会有。
需要注册为@Service
//UserDao是DAO层,用于数据库交互,具体使用可以去了解Mybatis。
@Service
public class UserDetailsServiceImpl implements UserDetailsService{
@Resource
UserDao userDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1.查询用户
User user = userDao.findByName(username);
// 2.判断用户是否存在
if (user == null) {
throw new UsernameNotFoundException(username);
}
//TODO 3.返回UserDetails实现类
return new UserDetailsImpl(user); // UserDetailsImpl 是我们实现的类
}
}
若我们需要自己的用户类型,写一个新的UserDetails实现类即可。此处为UserDetailsImpl
内部引用了用户类型User,实现了UserDetails中要求的方法,若有其他需求也可自行添加,这里不尝试
public class UserDetailsImpl implements UserDetails{
private User user; // 通过有参构造函数填充赋值的
public UserDetailsImpl(User user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of();
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getName();
}
@Override
public boolean isAccountNonExpired() { // 检查账户是否 没过期。
return true;
}
@Override
public boolean isAccountNonLocked() { // 检查账户是否 没有被锁定。
return true;
}
@Override
public boolean isCredentialsNonExpired() { //检查凭据(密码)是否 没过期。
return true;
}
@Override
public boolean isEnabled() { // 检查账户是否启用。
return true;
}
public User getUser() {
return user;
}
}
总结:
UserDetailsServiceImpl覆写UserDetailsService,自定义用户读取方法,返回UserDetails,注册为Service;
UserDetailsImpl实现UserDetails,内部引用所需User对象。
3. 加密配置
3.1 加密接口
SpringSecurity通过 PasswordEncoder 接口定义了加密相关方法。
public interface PasswordEncoder {
String encode(CharSequence rawPassword);
boolean matches(CharSequence rawPassword, String encodedPassword);
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
它本身提供了一些实现,如需使用可查看接口实现。

3.2 使用方式
在SecurityConfig.java中注册PasswordEncoder的Bean
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); //这里选用提供的BCryptPasswordEncoder实现,该实现采用的是BCrypt算法
}
注意:在你注册PasswordEncoder之后,如果你没有重写它的验证相关实现类,通过SpringSecurity登录会默认进行加密对比。
二. SpringSecurity默认加载的15个过滤器
//0-4 这几个过滤器是 功能性的前置过滤器,提供了SpringSecurity的基础必要能力。
0-DisableEncodeUrlFilter
1-WebAsyncManagerIntegrationFilter
2-SecurityContextHolderFilter
3-HeaderWriterFilter
4-CsrfFilter
//5-14 则与认证和授权过程相关
5-LogoutFilter
6- UsernamePasswordAuthenticationFilter
7-DefaultLoginPageGeneratingFilter
8-DefaultLogoutPageGeneratingFilter
9-BasicAuthenticationFilter
10-RequestCacheAwareFilter
11-SecurityContextHolderAwareRequestFilter
12-AnonymousAuthenticationFilter
13-ExceptionTranslationFilter
14-AuthorizationFilter
1. DisableEncodeUrlFilter
禁止URL重新编码
使用原因:
Session会话会在cookies中保存SessionID,
如果禁用了cookie,后端的默认响应会重写url将sessionId拼接到url后面,这样sessionId就在http访问日志中暴露
public class DisableEncodeUrlFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 对response进行包装后,继续执行后面的过滤器
filterChain.doFilter(request, new DisableEncodeUrlResponseWrapper(response));
}
private static final class DisableEncodeUrlResponseWrapper extends HttpServletResponseWrapper {
private DisableEncodeUrlResponseWrapper(HttpServletResponse response) {
super(response);
}
@Override
public String encodeRedirectURL(String url) {
// 不重新url 直接返回
return url;
}
@Override
public String encodeURL(String url) {
// 不重新url 直接返回
return url;
}
}
}
2. WebAsyncManagerIntegrationFilter
Web异步处理整合过滤器
默认情况下securityContextHolderStrategy的存储策略为ThreadLocal,在ThreadLocal的存储策略下,只有当前线程可以获取到securityContextHolder。
WebAsyncManagerIntegrationFilter 通过创建拦截器的形式,将securityContextHolderStrategy传递给子线程,后续子线程可以通过该拦截器获取到用户认证信息
public final class WebAsyncManagerIntegrationFilter extends OncePerRequestFilter {
private static final Object CALLABLE_INTERCEPTOR_KEY = new Object();
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
.getContextHolderStrategy();
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 尝试获取一个 Object asyncManagerAttr = servletRequest.getAttribute(WEB_ASYNC_MANAGER_ATTRIBUTE);
// asyncManagerAttr 如果已存在,返回已存在的对象,
// asyncManagerAttr 为空,并将新对方放到servletRequest.setAttribute(WEB_ASYNC_MANAGER_ATTRIBUTE, asyncManager);
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
// 尝试从asyncManager 中获取已经存储的拦截器
SecurityContextCallableProcessingInterceptor securityProcessingInterceptor = (SecurityContextCallableProcessingInterceptor) asyncManager
.getCallableInterceptor(CALLABLE_INTERCEPTOR_KEY);
// 如果没有获取到
if (securityProcessingInterceptor == null) {
// 新建一个拦截器
SecurityContextCallableProcessingInterceptor interceptor = new SecurityContextCallableProcessingInterceptor();
// 将SecurityContextHolderStrategy存储到拦截器,供子线程可获取
interceptor.setSecurityContextHolderStrategy(this.securityContextHolderStrategy);
// 注册拦截器放到asyncManager中
asyncManager.registerCallableInterceptor(CALLABLE_INTERCEPTOR_KEY, interceptor);
}
// 继续后续拦截器执行
filterChain.doFilter(request, response);
}
}
3. SecurityContextHolderFilter
持有SecurityContext的过滤器。
该Filter是用于存储用户认证信息的,他有两个重要的属性SecurityContextRepository和securityContextHolderStrategy。
public class SecurityContextHolderFilter extends GenericFilterBean {
// SecurityContextRepository接口 提供一种在整个请求上下文存储SecurityContext的能力
// SecurityContextRepository接口有两个重要方法: loadContext - 获取SecurityContext loadDeferredContext-延期获取SecurityContext saveContext - 保存SecurityContext
// 该属性默认为 DelegatingSecurityContextRepository,DelegatingSecurityContextRepository也是实现SecurityContextRepository接口的一个代理类
// DelegatingSecurityContextRepository允许代理多个SecurityContextRepository来实现SecurityContext的存储
// DelegatingSecurityContextRepository对 loadContext 和 saveContext 实现
// 默认被代理的SecurityContextRepository为:HttpSessionSecurityContextRepository 和 RequestAttributeSecurityContextRepository
// 当调用 DelegatingSecurityContextRepository 时,他会遍历被代理的SecurityContextRepository
// saveContext时:遍历被代理的SecurityContextRepository 都调用saveContext SecurityContext进行存储
// loadContext时: 调用自身loadDeferredContext 获取SecurityContext
// loadDeferredContext时:遍历被代理的SecurityContextRepository 调用loadDeferredContext 获取 SecurityContext
private final SecurityContextRepository securityContextRepository;
// 在线程中存储SecurityContext的策略
// 默认都是ThreadLocal。
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
.getContextHolderStrategy();
}
SecurityContextHolderFilter是第三个要执行的Filter,但是他是直接继承于GenericFilterBean。
该过滤器最主要的作用是:
如果请求上下文中存在 SecurityContext 则 SecurityContext存储到securityContextHolderStrategy默认是ThreadLocal
并在整个过滤器链执行完成后清除SecurityContext
存储到securityContextHolderStrategy可以保证后续的过滤器都可以从securityContextHolderStrategy中获取到SecurityContext。
doFilter逻辑:
public class SecurityContextHolderFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
// 如果已经执行过 该过滤器
if (request.getAttribute(FILTER_APPLIED) != null) {
// 跳过 直接执行下一个过滤器
chain.doFilter(request, response);
return;
}
// 标记改过滤器在本次请求过程中已经执行过
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
// 从SecurityContextRepository获取到SecurityContext
Supplier<SecurityContext> deferredContext = this.securityContextRepository.loadDeferredContext(request);
try {
// 将SecurityContext存储到securityContextHolderStrategy中,也就是存储到线程中。
this.securityContextHolderStrategy.setDeferredContext(deferredContext);
// 继续执行Filter
chain.doFilter(request, response);
}
finally {
// 这时应该 后续所有的Filter都已执行完后,有回到当前Filter中
// 请求执行完成后,清除SecurityContext
this.securityContextHolderStrategy.clearContext();
// 移除已执行标记
request.removeAttribute(FILTER_APPLIED);
}
}
}
4. HeaderWriterFilter
头信息写入过滤器
为当前响应添加报头的过滤器实现。可以添加某些头,启用浏览器保护。像X-Frame-Options, X-XSS-Protection和X-Content-Type-Options。
public class HeaderWriterFilter extends OncePerRequestFilter {
// 要写人的头信息
private final List<HeaderWriter> headerWriters;
// 默认是false 也就是在过滤器都执行完成后,回到该过滤器时向response中写入
private boolean shouldWriteHeadersEagerly = false;
// 构造方法,需要在构造该过滤器时就传入要写入ResponseHeader的头信息
// 具体确定要写入那些头信息是由HeadersConfigurer来决定的
// 默认是以下几个头信息
// 0 = {XContentTypeOptionsHeaderWriter} X-Content-Type-Options: nosniff
// 1 = {XXssProtectionHeaderWriter} X-XSS-Protection: 0
// 2 = {CacheControlHeadersWriter}
// Header [name: Cache-Control, values: [no-cache, no-store, max-age=0, must-revalidate]]
// Header [name: Pragma, values: [no-cache]]
// Header [name: Expires, values: [0]]
// 3 = {HstsHeaderWriter} Strict-Transport-Security: max-age=31536000 ; includeSubDomains
// 4 = {XFrameOptionsHeaderWriter} X-Frame-Options: DENY
public HeaderWriterFilter(List<HeaderWriter> headerWriters) {
Assert.notEmpty(headerWriters, "headerWriters cannot be null or empty");
this.headerWriters = headerWriters;
}
// 具体的过滤器执行方法
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (this.shouldWriteHeadersEagerly) {
// 如果是提前写入响应头,则是直接调用了writeHeaders 方法,并继续执行过滤器
doHeadersBefore(request, response, filterChain);
}
else {
// 默认走该方法
// 再过滤器执行完成后,再写入头信息
doHeadersAfter(request, response, filterChain);
}
}
private void doHeadersBefore(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws IOException, ServletException {
// 写入头信息
writeHeaders(request, response);
// 执行过滤器
filterChain.doFilter(request, response);
}
private void doHeadersAfter(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws IOException, ServletException {
// 对Req 和 Resp进行一个包装
HeaderWriterResponse headerWriterResponse = new HeaderWriterResponse(request, response);
HeaderWriterRequest headerWriterRequest = new HeaderWriterRequest(request, headerWriterResponse);
try {
// 先执行后续过滤器
filterChain.doFilter(headerWriterRequest, headerWriterResponse);
}
finally {
// 执行完后续过滤器,回到该过滤器后,写入响应头信息
headerWriterResponse.writeHeaders();
}
}
void writeHeaders(HttpServletRequest request, HttpServletResponse response) {
for (HeaderWriter writer : this.headerWriters) {
// 将头信息写入HttpServletResponse
writer.writeHeaders(request, response);
}
}
}

浙公网安备 33010602011771号