Spring Security从hello world开始

216 阅读5分钟

文章内容

本章水文, 就简单的介绍了spring secuirty怎么写个hello world, 简单分析了点源码

是什么?

spring security帮我们做了: 你是谁, 和你能做什么

认证 + 授权 + 安全(防火墙)

hello world

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
@RestController
@Slf4j
public class HelloController {
	
    @GetMapping("hello")
    public String hello() {
    	return "hello spring security";
    }
    
}

直接启动项目就好了, 你现在访问 /hello 就会被重定向到 /login

用户名默认为 user, 密码在控制台中, 本质是由 UUID 生成出来的

如果嫌每次都要去控制台找密码麻烦, 可以添加配置

server:
  port: 8080
spring:
  application:
    name: hello-world-demo
  security:
    user:
      name: zhazha
      password: "{noop}123456"

添加用户名和密码就行, 如此你就可以使用上面的用户名密码了

{noop} 后续会讲, 这里就认为密码是 123456

背后做了什么?

你看到什么, 他就配置了什么

  1. 看到了它使用了我们定义的用户名和密码
  2. 看到了重定向
  3. 看到了/login登录页面
  4. 看到了/logout注销页面

image-20221113085111296

在哪里拿到我们配置的用户名和密码?

@AutoConfiguration
@ConditionalOnClass(AuthenticationManager.class)
@ConditionalOnBean(ObjectPostProcessor.class)
@ConditionalOnMissingBean(
      value = { AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class,
            AuthenticationManagerResolver.class },
      type = { "org.springframework.security.oauth2.jwt.JwtDecoder",
            "org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector",
            "org.springframework.security.oauth2.client.registration.ClientRegistrationRepository",
            "org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository" })
public class UserDetailsServiceAutoConfiguration {

   private static final String NOOP_PASSWORD_PREFIX = "{noop}";

   private static final Pattern PASSWORD_ALGORITHM_PATTERN = Pattern.compile("^\{.+}.*$");

   private static final Log logger = LogFactory.getLog(UserDetailsServiceAutoConfiguration.class);

   @Bean
   @Lazy
   public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties,
         ObjectProvider<PasswordEncoder> passwordEncoder) {
      SecurityProperties.User user = properties.getUser();
      List<String> roles = user.getRoles();
      return new InMemoryUserDetailsManager(
            User.withUsername(user.getName()).password(getOrDeducePassword(user, passwordEncoder.getIfAvailable()))
                  .roles(StringUtils.toStringArray(roles)).build());
   }

   private String getOrDeducePassword(SecurityProperties.User user, PasswordEncoder encoder) {
      String password = user.getPassword();
      if (user.isPasswordGenerated()) {
         logger.warn(String.format(
               "%n%nUsing generated security password: %s%n%nThis generated password is for development use only. "
                     + "Your security configuration must be updated before running your application in "
                     + "production.%n",
               user.getPassword()));
      }
      if (encoder != null || PASSWORD_ALGORITHM_PATTERN.matcher(password).matches()) {
         return password;
      }
      return NOOP_PASSWORD_PREFIX + password;
   }

}

image.png

spring security 在这个地方读取了我们的用户名和密码 在内存中创建了我们的账户, 具体保存在这里:

image.png

后续的增删改查都是关于Map的操作, 不用分析了

通过分析这段源码我们能知道什么?

如果我们自己配置了

AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class,
      AuthenticationManagerResolver.class

中的一个, 那么上面那个配置类就会失败

User.withUsername 可以用来创建一个 UserDetails 对象

  1. InMemoryUserDetailsManager

image.png

两个重点接口: UserDetailsServiceUserDetailsPasswordService

image.png

用于查找UserDetails用户信息, 我们可以实现这个接口, 改从数据库中读取

image.png

这个接口用于更新用户加密方式, 比如你现在是MD5, 哪天MD5出现了巨大的安全问题, 此时需要升级加密算法, 这个接口就能够用到了

当然, 肯定不是这么简单的, 后面会讲

重定向源码细节?

怎么入手呢? 如果我不去看spring security官方文档上的图片和介绍的类, 要怎么入手分析这段源码?

追根溯源, 重定向是谁执行的? 找HttpServletResponse, 下断

image.png

小白: "你这断点下的不行, 我重定向了好几回都没重定向到我想要的url上"
小黑: "那是你傻, 你可以先把断点去掉, 等到快要重定向时再把断点打开"
逆向分析同学: "这我熟"

断点断这

image.png

然后往调用栈里找

image.png

看到异常了

image.png

看到上面的stackTrace, 发现还是Filter, 说明没找到点上, 再往上一个调用栈里找找, 直到

image.png

小白: "发现是Filter为什么要继续找?"
小黑: "因为FilterChain保不齐哪个Filter就会重新包装exception"

说明找到点上了

image.png

从图片可以看出, 遍历一个集合, 集合内存储了很多的voter投票器, 然后投票器有三种返回值

  • default(实际是弃权状态)
  • 授权
  • 拒绝
int ACCESS_GRANTED = 1;
int ACCESS_ABSTAIN = 0;
int ACCESS_DENIED = -1;

稍稍看下底层代码

image.png

两个集合轮询比较

只有

attribute.getAttribute().equals(authority.getAuthority())

的表达式结果为 true 才会允许访问我们想要访问的url

小白: "看不懂"
小黑: "看懂这个很简单, 点明上面两个集合, 一个是当前已登录用户拥有的权限集合, 另一个是你需要访问目标所需要的权限集合"
小黑: "比如你要访问 /user , 这算是资源, 该资源需要你拥有admin或user权限才可以访问"
小白: "也就是说上面其实就是判断我有没有权限访问该资源咯, 我还以为只要判断服务器中没有你的session就需要重定向了"
小白: "但是为什么啊? 不是都没人登录么? 还怎么获得当前登录用户的权限?"
小黑: "spring security默认会登录一个匿名用户, 匿名用户也是有属于自己的权限"

image.png

小白: "那我总结下, 因为当前用户找不到目标资源所需的权限, 所以抛出AccessDeniedException, 被spring security的异常过滤器捕获, 发现是没权限异常又判断出来是匿名方式登录, 走重定向/login流程"

登录页生成源码

先看一大坨过滤器, 这是 spring security过滤器执行的顺序:

ForceEagerSessionCreationFilter
ChannelProcessingFilter
WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter
HeaderWriterFilter
CorsFilter
CsrfFilter
LogoutFilter
OAuth2AuthorizationRequestRedirectFilter
Saml2WebSsoAuthenticationRequestFilter
X509AuthenticationFilter
AbstractPreAuthenticatedProcessingFilter
CasAuthenticationFilter
OAuth2LoginAuthenticationFilter
Saml2WebSsoAuthenticationFilter
UsernamePasswordAuthenticationFilter
DefaultLoginPageGeneratingFilter
DefaultLogoutPageGeneratingFilter
ConcurrentSessionFilter
DigestAuthenticationFilter
BearerTokenAuthenticationFilter
BasicAuthenticationFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
JaasApiIntegrationFilter
RememberMeAuthenticationFilter
AnonymousAuthenticationFilter
OAuth2AuthorizationCodeGrantFilter
SessionManagementFilter
ExceptionTranslationFilter
FilterSecurityInterceptor
SwitchUserFilter

官方文档在: Architecture :: Spring Security

在上面的顺序中你会看到这两个过滤器

DefaultLoginPageGeneratingFilter
DefaultLogoutPageGeneratingFilter

他会帮我们生成登录页和注销页

可以进去瞄一眼源码, 但没必要纠结

唯一需要注意的一点是: 登录页前端怎么显示异常的?

image.png

image.png

所以我们只要在前端这么搞就行咯

image.png

总结

学到了什么? 啥也没有, 凉凉~