Spring Security 自定义认证管理器和讲解(二)

目录

一、概述

二、自定义认证管理器

小结

三、Spring Security 核心组件

小结


Spring Security 学习专栏

 

1. Spring Security 入门学习(一)

2. Spring Security 自定义认证管理器和讲解 (二)

3. Spring Security 接口详解 (三)

4. Spring Security 工作原理 (四)

 

 


一、概述

上一篇Spring Security 入门学习带大家搭建一个简单Demo,认识SpringSecurity,这篇文章讲自定义认证管理器和讲解

 

二、自定义认证管理器

Spring Security为我们提供了一个「httpBasic」模式的简单登陆页面,并在控制台输出了密码(这个密码每次启动都是不一样的)。如果我们想用自己的定义的账号密码,则需要改配置。如下:

1. SecurityConfig

并加入一些代码,如下所示:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {


    /** 
     * 认证管理器配置
     * @param auth
     * @date: 2021/3/11 17:39
     * @return: void 
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.
                // 使用内存中的 InMemoryUserDetailsManager
                        inMemoryAuthentication()
                // 不使用 PasswordEncoder 密码编码器
                .passwordEncoder(NoOpPasswordEncoder.getInstance())
                .withUser("root").password("root").roles("ADMIN","NORMAL")
                // 配置 admin 用户
                .and().withUser("admin").password("admin").roles("ADMIN")
                // 配置 normal 用户
                .and().withUser("normal").password("normal").roles("NORMAL");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http
                // 配置请求地址的权限
                .authorizeRequests()
                // 所有用户可访问,不需要登入
                .antMatchers("/test/echo").permitAll()
                // 需要 ADMIN 角色
                .antMatchers("/test/admin","/test/getUserInfo").hasRole("ADMIN")
                // 需要 NORMAL 角色
                .antMatchers("/test/normal","/test/getUserInfo").access("hasRole('NORMAL')") // 需要 NORMAL 角色。
                // 任何请求,访问的用户都需要经过认证
                .anyRequest()
                .authenticated()
                .and()
                // 设置 Form 表单登陆
                .formLogin()
                //.loginPage("/login") // 登陆 URL 地址
                .permitAll() // 所有用户可访问
                .and().logout()
                // .logoutUrl("/logout") // 退出 URL 地址
                // 所有用户可访问
                .permitAll()
                // 禁用跨域
                .and().csrf().disable();
    }
}

上面的配置其实和默认情况的配置几乎一样,只是这里定义了三个用户root、admin、normal 。(说明:passwordEncoder(NoOpPasswordEncoder.getInstance())  不使用 PasswordEncoder 密码编码器)此时我们启动项目,便可以使用root、admin、normal 都可以登入,不同的用户拥有不同角色所看的资源也不一样。

用户-角色-资源 访问控制

  • root 用户可以看到所有的资源
  • admin 用户可以看到 "/test/echo","/test/admin","/test/getUserInfo" 资源
  • normal 用户可以看到 "/test/echo","/test/normal","/test/getUserInfo" 资源

2. TestController

@Slf4j
@RestController
@RequestMapping(value = "/test")
public class TestController {

    @GetMapping("/echo")
    public String demo() {
        return "示例返回";
    }

    @GetMapping("/home")
    public String home() {
        return "我是首页";
    }

    @GetMapping("/admin")
    public String admin() {
        return "我是管理员";
    }

    @GetMapping("/normal")
    public String normal() {
        return "我是普通用户";
    }

    @GetMapping("/getUserInfo")
    public String getUserInfo(){

        String currentUser ;
        Object principl = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        if(principl instanceof UserDetails) {
            currentUser = ((UserDetails)principl).getUsername();
        }else {
            currentUser = principl.toString();
        }
        log.info("getUserInfo.resp currentUser is:{}",currentUser);
        return " currentUser is ===>: " +currentUser;
    }

}

3. 测试验证

下面来验证一下普通用户登录,重启项目,在浏览器中输入:http://127.0.0.1:8080/test/admin同样,我们会到达登录页面,我们输入用户名 normal,密码也为normal结果错误页面了,拒绝访问了,信息为:

我们把浏览器中的uri修改成:/test/normal,结果访问成功。可以看到 我是普通用户。说明 normal这个USER角色只能访问 "/test/echo","/test/normal","/test/userInfo" ,这个结果与我们预期一致。

再来验证一下超级管理员用户登录,重启浏览器之后,输入http://127.0.0.1:8080/test/admin。在登录页面中输入用户名root,密码root,提交之后,可以看到 我是管理员 ,说明访问管理员资源了。我们再将浏览器uri修改成 /test/normal,刷新之后,也能看到我是普通用户,说明 root用户可以访问所有资源,这个也和我们的预期一致。

 

4. 获取当前登录用户信息

上面我们实现了“资源 - 角色”的访问控制,效果和我们预期的一致,但是并不直观,我们不妨尝试在控制器中获取“当前登录用户”的信息,直接输出,看看效果。

/test/userInfo为例,我们修改其代码,如下:

@GetMapping("/getUserInfo")
    public String getUserInfo(){

        String currentUser ;
        Object principl = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        if(principl instanceof UserDetails) {
            currentUser = ((UserDetails)principl).getUsername();
        }else {
            currentUser = principl.toString();
        }
        log.info("getUserInfo.resp currentUser is:{}",currentUser);
        return " currentUser is ===>: " +currentUser;
    }

这里,我们通过SecurityContextHolder 来获取了用户信息,并拼接成字符串输出。重启项目,在浏览器访问http://127.0.0.1:8080/test/getUserInfo. 使用 admin的身份登入,可以看到浏览器显示 currentUser is: admin。使用 root 的身份登入,可以看到浏览器显示 currentUser is: root

 

小结

至此,我们已经对Spring Security有了一个基本的认识了。了解了如何在项目中加入spring security,以及如何控制资源的角色访问控制。spring security原不止这么简单,我们才刚刚开始。为了能够更好的在实战中使用spring security 我们需要更深入的了解。下面我们先来了解spring security的一些核心概念。

 

三、Spring Security 核心组件

Spring Security 核心组件有:SecurityContextSecurityContextHolderAuthenticationUserdetailsAuthenticationManager,下面分别介绍。


1. SecurityContext

安全上下文,用户通过Spring Security 的校验之后,验证信息存储在SecurityContext中,SecurityContext的接口定义如下:

public interface SecurityContext extends Serializable {
    /**
     * Obtains the currently authenticated principal, or an authentication request token.
     *
     * @return the <code>Authentication</code> or <code>null</code> if no authentication
     * information is available
     */
    Authentication getAuthentication();
 
    /**
     * Changes the currently authenticated principal, or removes the authentication
     * information.
     *
     * @param authentication the new <code>Authentication</code> token, or
     * <code>null</code> if no further authentication information should be stored
     */
    void setAuthentication(Authentication authentication);

}

可以看到 SecurityContext 接口只定义了两个方法,实际上其主要作用就是获取 Authentication 对象。

2. SecurityContextHolder

SecurityContextHolder 看名知义,是一个holder,用来hold住SecurityContext实例的。在典型的web应用程序中,用户登录一次,然后由其会话ID标识。服务器缓存持续时间会话的主体信息。在Spring Security中,在请求之间存储SecurityContext的责任落在SecurityContextPersistenceFilter上,默认情况下,该上下文将上下文存储为HTTP请求之间的HttpSession属性。它会为每个请求恢复上下文SecurityContextHolder,并且最重要的是,在请求完成时清除SecurityContextHolder。SecurityContextHolder是一个类,他的功能方法都是静态的(static)。

SecurityContextHolder可以设置指定JVM策略(SecurityContext的存储策略),这个策略有三种:

  •     MODE_THREADLOCAL:SecurityContext 存储在线程中。
  •     MODE_INHERITABLETHREADLOCAL:SecurityContext 存储在线程中,但子线程可以获取到父线程中的 SecurityContext。
  •     MODE_GLOBAL:SecurityContext 在所有线程中都相同。

SecurityContextHolder默认使用MODE_THREADLOCAL模式,即存储在当前线程中。在spring security应用中,我们通常能看到类似如下的代码:

 SecurityContextHolder.getContext().setAuthentication(token);

其作用就是存储当前认证信息。


3. Authentication

authentication 直译过来是“认证”的意思,在Spring Security 中Authentication用来表示当前用户是谁,一般来讲你可以理解为authentication就是一组用户名密码信息。Authentication也是一个接口,其定义如下:

public interface Authentication extends Principal, Serializable {
 
    Collection<? extends GrantedAuthority> getAuthorities();
    Object getCredentials();
    Object getDetails();
    Object getPrincipal();
    boolean isAuthenticated();
    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;

}

接口有4个get方法,分别获取

  •     Authorities, 填充的是用户角色信息。
  •     Credentials,直译,证书。填充的是密码。
  •     Details ,用户信息。
  •     Principal 直译,形容词是“主要的,最重要的”,名词是“负责人,资本,本金”。感觉很别扭,所以,还是不翻译了,直接用原词principal来表示这个概念,其填充的是用户名。

因此可以推断其实现类有这4个属性。这几个方法作用如下:

  •     getAuthorities: 获取用户权限,一般情况下获取到的是用户的角色信息。
  •     getCredentials: 获取证明用户认证的信息,通常情况下获取到的是密码等信息。
  •     getDetails: 获取用户的额外信息,(这部分信息可以是我们的用户表中的信息)
  •     getPrincipal: 获取用户身份信息,在未认证的情况下获取到的是用户名,在已认证的情况下获取到的是 UserDetails (UserDetails也是一个接口,里边的方法有getUsername,getPassword等)。
  •     isAuthenticated: 获取当前 Authentication 是否已认证。
  •     setAuthenticated: 设置当前 Authentication 是否已认证(true or false)。

4. UserDetails

UserDetails,看命知义,是用户信息的意思。其存储的就是用户信息,其定义如下:

public interface UserDetails extends Serializable {
 
    Collection<? extends GrantedAuthority> getAuthorities();
    String getPassword();
    String getUsername();
    boolean isAccountNonExpired();
    boolean isAccountNonLocked();
    boolean isCredentialsNonExpired();
    boolean isEnabled();

}

方法含义如下:

  •     getAuthorites:获取用户权限,本质上是用户的角色信息。
  •     getPassword: 获取密码。
  •     getUserName: 获取用户名。
  •     isAccountNonExpired: 账户是否过期。
  •     isAccountNonLocked: 账户是否被锁定。
  •     isCredentialsNonExpired: 密码是否过期。
  •     isEnabled: 账户是否可用。

5. UserDetailsService

提到了UserDetails就必须得提到UserDetailsService, UserDetailsService也是一个接口,且只有一个方法loadUserByUsername,他可以用来获取UserDetails

通常在spring security应用中,我们会自定义一个CustomUserDetailsService来实现UserDetailsService接口,并实现其public UserDetails loadUserByUsername(final String login);方法。我们在实现loadUserByUsername方法的时候,就可以通过查询数据库(或者是缓存、或者是其他的存储形式)来获取用户信息,然后组装成一个UserDetails,(通常是一个org.springframework.security.core.userdetails.User,它继承自UserDetails) 并返回。

在实现loadUserByUsername方法的时候,如果我们通过查库没有查到相关记录,需要抛出一个异常来告诉spring security来“善后”。这个异常是org.springframework.security.core.userdetails.UsernameNotFoundException。


6. AuthenticationManager

AuthenticationManager 是一个接口,它只有一个方法,接收参数为Authentication,其定义如下:

public interface AuthenticationManager {
    Authentication authenticate(Authentication authentication)
            throws AuthenticationException;

}

AuthenticationManager 的作用就是校验Authentication,如果验证失败会抛出AuthenticationException 异常。AuthenticationException是一个抽象类,因此代码逻辑并不能实例化一个AuthenticationException异常并抛出,实际上抛出的异常通常是其实现类,如DisabledException,LockedException,BadCredentialsException等。BadCredentialsException可能会比较常见,即密码错误的时候

 

项目代码

https://gitee.com/gaibianzlp/spring-security-demo.git


小结

这里,我们只是简单的了解了spring security中有哪些东西,先混个脸熟。这里并不需要我们一下子全记住这些名词和概念。先大概看看,有个印象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值