spring boot security 通俗理解 + 源码

以你之姓@ 2024-04-19 08:06 141阅读 0赞

本文内容适合刚接触spring security的新手,大神请跳过。spring security是一个用来保护spring应用程序的框架,它在用户访问web程序的时候会进行身份的认证(判断当前用户是谁)和授权(当前用户能访问哪些uri,不能访问哪些uri)。我们经常见到场景:1.访问某些网站时需要先登录用户名和密码;2.当你用自己的用户名密码登录某电商网站后,你只能浏览自己的订单,不能看别人的,而电商的某些运营人员或工程师可以看到所有用用户的订单;这个就是认证和授权。接下来我们结合一个Demo来具体看下认证和授权到底是个什么鬼,以及整个过程是怎么操作的(其实只需要重写三个方法就行了)。Demo源码

场景介绍:我们有一个controller,它包含两个方法,一个用来创建用户,另一个用来获取用户。uri如下:

  1. 创建用户:/users
  2. 获取用户:/users/{ username}

当我们在building.gradle文件中添加spring security的依赖时默认security自动开启保护。它做了两件事情,1.访问所有的uri都需要通过认证,没有用过认证的用户不能访问uri,通过认证没有达到该uri访问权限的用户也不能访问该uri;2.spring security自动生成一个用户,它的用户名是user,密码每次web服务启动时才会随机产生。如下图所示:

在这里插入图片描述

这种保护策略当然不是我们想要的,所以我们要自定义保护策略(地球人都是这么干的)。对于上面的两个uri我们规定:创建用户不需要认证和授权,获取用户的时候需要认证和授权。为了实现自定义策略,我们需要继承WebSecurityConfigurerAdapter类,一看名字它就是专门用来配置spring security的。那好我们先来看下它默认的配置长什么样子。代码如下:

  1. protected void configure(HttpSecurity http) throws Exception {
  2. http
  3. .authorizeRequests()
  4. .anyRequest().authenticated()
  5. .and()
  6. .formLogin().and()
  7. .httpBasic();
  8. }

这是WebSecurityConfigurerAdapter中的一个方法,它控制每个uri的访问权限,目前默认的配置是每个请求都需要认证,系统支持表单认证和httpbasic两种认证方式。要实现我们的自定一就必须重写这个方法,代码如下:

  1. @Override
  2. protected void configure(HttpSecurity http) throws Exception {
  3. http.
  4. .authorizeRequests()
  5. .antMatchers(HttpMethod.POST, "/users")
  6. .permitAll()
  7. .antMatchers(HttpMethod.GET, "/users/*")
  8. .hasRole("ADMIN")
  9. .and()
  10. .formLogin()
  11. .and()
  12. .httpBasic();
  13. }

permitAll()允许所有人访问/users,而只有具有ADMIN角色的用户才能访问/users/*同样支持表单和httpBasic两种认证方式,这就是spring security的安全性配置。

我们能成功登录电商网站(认证通过)是因为网站里存储了们的账户名和密码,系统拿我们输入的用户名密码跟后台已存储的进行比对,若都一致则登录成功(认证通过)。而此时系统只有一个user用户,密码是随机的,那我们想让用户名:admin,密码:password的用户能通过认证并且能访问/users/{username}该怎么办呢?只需要添加用户存储就可以了,spring security提供了基于内存的用户存储基于数据库的用户存储两种方式。因为是Demo展示,所以本文选择基于内存的用户存储。仍旧是重写方法,代码如下:

  1. @Override
  2. protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  3. auth.inMemoryAuthentication()
  4. .passwordEncoder(new BCryptPasswordEncoder())
  5. .withUser("admin")
  6. .password(new BCryptPasswordEncoder().encode("password"))
  7. .roles("ADMIN")
  8. .and()
  9. .withUser("user")
  10. .password(new BCryptPasswordEncoder().encode("123"))
  11. .roles("USER");
  12. }

上述代码在内存中创建了两个用户,信息如下:





















用户名 密码 角色
admin password ADMIN
user 123 USER

此时在系统内部就已经存在了两个用户(当重写此方法后spring security就不会再默认生成用户了),当外部输入的用户跟二者一致能匹配上时,认证就通过了。

接下来就是最后一个问题了,认证过程是怎么执行的。当初这块也来回看了好几次,最后是根据别人些的博客外加源码打断调试才搞明白的。简单来说认证过程大多数工作系统代码已经帮我们做了,我们只需要关心UserDetailsUserDetailsService这两个都是接口,前者的实现类User用来记录”找到的”用户的详细信息,后者里面只有一个方法loadUserByUsername,这个方法是用来实现”找” 这个步骤的,接下来机进行详细解释。

UserDetailsService接口:

  1. public interface UserDetailsService {
  2. UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
  3. }

因为Demo是基于内存的用户存储,所以我们来看UserDetailsService的一个实现类InMemoryUserDetailsManager中的loadUserByUsername方法怎么重写的,代码如下:

  1. public class InMemoryUserDetailsManager implements UserDetailsManager,
  2. UserDetailsPasswordService {
  3. private final Map<String, MutableUserDetails> users = new HashMap<>();
  4. public UserDetails loadUserByUsername(String username)
  5. throws UsernameNotFoundException {
  6. UserDetails user = users.get(username.toLowerCase());
  7. if (user == null) {
  8. throw new UsernameNotFoundException(username);
  9. }
  10. return new User(user.getUsername(), user.getPassword(), user.isEnabled(),
  11. user.isAccountNonExpired(), user.isCredentialsNonExpired(),
  12. user.isAccountNonLocked(), user.getAuthorities());
  13. }
  14. }

当应用程序启动时,内存中的那两个用户会用来初始化成员变量users其中key是用户名,value是用户名对应的用户封装成MutableUserDetails(MutableUser是UserDetails的一个实现类)也就是说系统的已存用户都在users中。接着,用户提交的用户名和密码会被封装成Authentication(这是个接口,大多情况下都用它的实现类UsernamePasswordAuthenticationToken这个实现类有两个成员变量principalcredentials,前者用来存储用户名,后者存储密码)通过一系列的逻辑,Authenticationprincipal送到loadUserByUsername方法中,没错,上面的入口参数username就是principal。之后就是根据username在users中找有没有相同的,没有就报异常,有的话就将其封装成User(这也是UserDetails的一个实现类)返回。这就是上面说的”找到了”,具体通没通过认证呢?后面还会有个方法再校验User中的password,如果密码匹配则校验才算通过,不过密码校验系统已经替我们把代码写了。

上述是基于内存的的用户认证,所以直接就使用了InMemoryUserDetailsManager,如果是基于mysql的用户认证,可以自己写一个类继承UserDetailsService并实现loadUserByUsername方法根据username从mysql中读取数据再封装成User

Demo中是有一些测试:
测试方法:should_create_user_succeed()是创建用户,不需要认证和授权名,所以直接返回201。
测试方法:should_query_user_succeed()是获取用户, 因为是admin访问,所以请求成功,返回200。
测试方法:should_401_if_query_user_with_Unauthorized_user()是获取用户,因为密码错误,所以未通过认证,返回401。
测试方法:should_403_if_query_user_with_no_admin()是获取用户,认证通过了,但没有ADMIN角色,所以返回403。

就先写到着吧,spring security还有好几个东西,等有时间再一点点补齐。建议看下spring mvc中一个请求的执行过程,理解下拦截器和过滤器,会更加清楚spring security的执行过程。

发表评论

表情:
评论列表 (有 0 条评论,141人围观)

还没有评论,来说两句吧...

相关阅读

    相关 Spring boot security

      本案例通过mybatis为持久层,自定义了用户和配套权限,在请求Spring boot web的controller方法时做不同权限的控制。代码在https://githu