web-security 第三期:畅谈 Spring Security Authentication (认证)

所有的安全框架都有两个很重要的组成部分,认证 和  授权 ,简单的说,认证就是判断你是谁,授权就是你有权限干啥,这一期我们先来谈一谈Spring Security Authentication (认证方式) ,本节源码地址 (spring-security模块)

目录

1.从一个极简的例子开始(基于内存的认证)

2.基于JDBC的内存认证

2.1.UserDetails

2.2.UserDetailsService

2.3.PasswordEncoder

2.4. DaoAuthenticationProvider

3. 记住我

 


1.从一个极简的例子开始(基于内存的认证)

首先完成Security的基本配置:

/**
 * @author swing
 */
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
      http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .authorizeRequests()
                //允许匿名访问的api,其他的需要验证
                .antMatchers("/login", "/login/page").permitAll()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();
    }

    /**
     * 将认证信息存储在内存中(这里相当于初始化了两个用户信息)
     *
     * @return 认证信息
     */
    @Bean
    public UserDetailsService users() {
        UserDetails user = User.builder()
                .username("user")
                .password(bCryptPasswordEncoder().encode("112233"))
                .roles("USER")
                .build();
        UserDetails admin = User.builder()
                .username("admin")
                .password(bCryptPasswordEncoder().encode("112233"))
                .roles("USER", "ADMIN")
                .build();
        return new InMemoryUserDetailsManager(user, admin);
    }

    /**
     * 解决 无法直接注入 AuthenticationManager
     *
     * @return AuthenticationManager
     * @throws Exception 异常
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

然后配置登录接口:

    @Resource
    private AuthenticationManager authenticationManager;
     
    @PostMapping
    @ResponseBody
    Object loginIn(String username, String password) {
        //验证用户名和密码
        Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
        return authentication.getPrincipal();
    }

测试结果:

POST http://localhost:8080/login?username=admin&password=112233

response:
{
  "password": null,
  "username": "admin",
  "authorities": [
    {
      "authority": "ROLE_ADMIN"
    },
    {
      "authority": "ROLE_USER"
    }
  ],
  "accountNonExpired": true,
  "accountNonLocked": true,
  "credentialsNonExpired": true,
  "enabled": true
}

Response code: 200; Time: 455ms; Content length: 198 bytes



POST http://localhost:8080/login?username=admin&password=11223


{
  "timestamp": "2020-06-10T09:50:04.327+00:00",
  "status": 403,
  "error": "Forbidden",
  "trace": "org.springframework.security.authentication.BadCredentialsException: Bad credentials"
  "message": "Access Denied",
  "path": "/login"
}

Response code: 403; Time: 85ms; Content length: 9274 bytes

2.基于JDBC的内存认证

前面是认证用户密码的最简单的例子,当然在实际开发中我们并不会这样做,这是基于内存的认证,即用户名和密码等信息是存储在内存中的,实际开发中当然是将他们放在数据库中, 也就是基于JDBC的认证,这里先介绍几个类

2.1.UserDetails

顾名思义,这个类是存储的是用户的详细信息:(结构大概如下)

{
  "password": null,
  "username": "admin",
//用户角色(角色是权限的集合)
  "authorities": [
    {
      "authority": "ROLE_ADMIN"
    },
    {
      "authority": "ROLE_USER"
    }
  ],
  "accountNonExpired": true,
  "accountNonLocked": true,
  "credentialsNonExpired": true,
  "enabled": true
}

Spring Security 已经为我们提供了这个接口的默认实现,即上面的User类,但由于各个项目的用户所有信息不尽相同,所以可以继承此接口,自定义UserDetails,如下:

public class UserDetailsImpl implements UserDetails {
    /**
     * 数据库用户信息(将其包装在userDetails中)
     */
    private UserDO userDO;

    /**
     * 返回授予用户的权限
     *
     * @return 权限集合
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    @Override
    public String getPassword() {
        return userDO.getPassword();
    }

    @Override
    public String getUsername() {
        return userDO.getUsername();
    }

    /**
     * 账户是否未过期,过期无法验证
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * 指定用户是否解锁,锁定的用户无法进行身份验证
     */
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     * 指示是否已过期的用户的凭据(密码),过期的凭据防止认证
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * 是否可用 ,禁用的用户不能身份验证
     */
    @Override
    public boolean isEnabled() {
        return true;
    }
}

2.2.UserDetailsService

DaoAuthenticationProvider使用UserDetailsS​​ervice检索用户名,密码和其他用于使用用户名和密码进行身份验证的属性。 Spring Security提供UserDetailsS​​ervice的内存中(InMemoryUserDetailsManager)和JDBC(JdbcUserDetailsManager)实现。你也可以自定义UserDetailsService 然后将其注册为Bean,如下:

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Resource
    private UserService userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {        
        UserDO user = userService.getUserByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException(username + ":该用户不存在!");
        }
        return new UserDetailsImpl(user);
    }
}

2.3.PasswordEncoder

 Spring Security 默认是不允许明文存储密码,并且其提供了许多加密方式

{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG 
{noop}password 
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc 
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=  
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0 

如需要指定当前项目的密码加密模式,如下两种方式都可:

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

     @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }

回顾一下,会发现基于JDBC的认证相当于只是修改了认证信息的获取路径,从内存转变到数据库(这里简化例子,就不使用具体的数据库操作了)Spring Security其实也为我们提供了官方参考的表结构,以后会说到。

2.4. DaoAuthenticationProvider

上面配置可以正常的完成用户验证,但这是这么做到的呢? 

DaoAuthenticationProvider is an AuthenticationProvider implementation that leverages a UserDetailsService and PasswordEncoder to authenticate a username and password.

usernamepasswordauthenticationfilter

daoauthenticationprovider

这里着重说一下重要的两个环节

  • 当请求到达过滤器 UsernamePasswordAuthenticationFilter 时,开始进行用户信息的验证
  • 认证成功的结果是什么呢?即图二的第五步,身份验证成功后,返回的身份验证的类型为UsernamePasswordAuthenticationToken,其主体为已配置的UserDetailsS​​ervice返回的UserDetails。最后,将通过authentication Filter 在SecurityContextHolder上设置返回的UsernamePasswordAuthenticationToken。

3. 记住我

我们先来讨论几个类:

securitycontextholder

 SecurityContextHolder 是 Spring Security身份验证模型的核心,用于存储通过身份验证的人员的详细信息。 Spring Security并不关心如何填充SecurityContextHolder。如果它包含一个值,那么它将用作当前经过身份验证的用户,而观察前面的认证过程,也就是向SecurityContextHolder中填充值的过程。

那么有一个很奇怪的问题,如果我第一个请求(/login)正确认证了信息,那么为什么我第二个请求就不能被服务器认证了?

应为Http是无状态的,所以你的每个请求对于服务器来说都是陌生的请求,服务器必须认证后才让你访问资源。因此SecurityContextHolder中的值在每个请求结束后都需要被清理,然后再认证,再填充,由于每个请求是一个跨越多个方法的线程,因此SecurityContextHolder使用ThreadLocal存储这些详细信息。官方文章这样描述:

By default the SecurityContextHolder uses a ThreadLocal to store these details, which means that the SecurityContext is always available to methods in the same thread of execution, even if the SecurityContext is not explicitly passed around as an argument to those methods. Using a ThreadLocal in this way is quite safe if care is taken to clear the thread after the present principal’s request is processed. Spring Security’s FilterChainProxy ensures that the SecurityContext is always cleared.

而在此请求的线程作用域内,可以随时获取Authentication,如下:

SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
String username = authentication.getName();
Object principal = authentication.getPrincipal();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

但如果每个请求都需要携带密码,那可又麻烦又不安全,如何才能让程序在一段时间内记住我呢?

很简单:认证成功后,让后台给我分配一个令牌(token),这个令牌存有我的密码和其他信息(加密过)接下来的每次请求,我都带上这个令牌(有效期内),在servlet容器中建立一个过滤器,在请求到达UserpasswrodAuthenticationFilter之前,完成我的验证,然后将认证结果填写在SecurityContextHolder中,那么此请求便可以访问资源了,如下实例片段(我这里使用的还是密码验证,实际开发使用令牌)下一期,我将使用JWT来实现上面阐述的令牌功能

@Component
public class AuthenticationFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken("swing", "123456");
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        filterChain.doFilter(request, response);
    }
}

        //将认证设置在UsernamePasswordAuthenticationFilter之前
        http.addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter.class);

 

猜你喜欢

转载自blog.csdn.net/qq_42013035/article/details/106630030
今日推荐