Spring Security 学习笔记(一)——认证到授权从入门到精通

一、Hello Spring Security

1.1. 创建一个Spring Security 项目

使用IDEA的Spring Initializr 向导创建SpringBoot项目,导入Web和Spring Security依赖

打开pom.xml,核心就是引入以下的依赖:

    <dependencies>
        <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>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

新建一个controller方便后续的测试

package cn.quguai.controller;

@RestController
public class HelloController {

    @GetMapping("/")
    public String hello(){
        return "Hello Spring Security";
    }
}

直接运行项目即可。并访问http://localhost:8080,可以看到下面的页面。用户名为user,密码来源于项目的控制台,是项目随机生成的。
image.png
image.png
image.png:浏览器显示的内容

除了以上的方式,还可以通过修改配置文件中的内容来自定义用户名和密码

spring.security.user.name=admin
spring.security.user.password=12345
spring.security.user.roles=admin

注意:以上的方式仅是针对于刚入门的教程,绝大多数web应用并不会采用这种HTTP基本认证服务,除了安全性差。无法携带cookie的基本信息外,灵活性差也是一个主要的缺点。通常来讲大家更愿意选择表单验证,自己实现表单验证和,从而提高安全性。

二、表单验证

2.1 默认表单验证

使用Spring Initializr 初始化一个SpringBoot项目,并新建一个配置类。
image.png
自定义的配置类需要去继承WebSecurityConfigurerAdapter并增加@EnableWebSecurity注解,其中该注解点开以后会发现已经有了@Configuration注解,相当于已经加入到容器当中。

@EnableWebSecurity  // 包含了@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

}

2.2 自定义表单登录页

image.png

  1. 新建一个controller来处理我们登录成功以后的请求
@RestController
public class MyController {

    @GetMapping("/myLogin")
    public String myLogin(){
        return "Login Success!!";
    }
}
  1. 初步配置自定义表单登陆页

此时我们就需要重写configure方法

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                    .loginPage("/login.html") // 自定义登录页
                    .loginProcessingUrl("/login")  // form表单登录action
                    .defaultSuccessUrl("/myLogin").permitAll() // 登陆成功后的请求
                .and().authorizeRequests()
                    .antMatchers("/css/**", "/img/**", "/js/**").permitAll() // 不对这些请求认证
                    .anyRequest().authenticated() // 其他所有请求都需要认证
                .and()
                    .csrf().disable(); // 关闭crsf拦截请求
    }
}
  1. 编写自定义登陆页面
<form action="/login" method="post">
		<input name="username" type="text" placeholder="手机号/邮箱">
		<input name="password" type="password" placeholder="密码">
		<input type="submit" value="登录">
</form>

以上这是列出核心的html,还可以自己进行丰富,主要前两个input框的name必须是usernamepassowrd。以上的页面放入到resources/static目录下。

三、认证与授权

3.1 默认数据库模型的认证与授权

1. 资源准备

在controller下新建三个Controller,分别代表三种场景,admin(管理员)、User(普通用户)、App(开放请求)。
image.png

@RestController
@RequestMapping("/admin")
public class AdminController {

    @GetMapping("api")
    public String hello(){
        return "admin Hello";
    }
}

@RestController
@RequestMapping("/user")
public class UserController {

    @GetMapping("api")
    public String hello(){
        return "user Hello";
    }
}

@RestController
@RequestMapping("/app")
public class AppController {

    @GetMapping("api")
    public String hello(){
        return "app Hello";
    }
}

2. 资源授权相关配置

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.formLogin()
                .and().authorizeRequests()
                .antMatchers("/app/api/**").permitAll()
                .antMatchers("/admin/api/**").hasRole("ADMIN")
                .antMatchers("/user/api/**").hasRole("USER")
                .anyRequest().authenticated();
    }
}

antMatchers()是一个采用ANT模式的URL匹配器。ANT模式使用可以匹配任意单个字符,使用*来匹配0个或多个字符。此时我们我们没有进行授权的相关用户操作,下面开始进行测试,启动服务。

访问 /app/api 请求无需进行登录认证

image.png

当访问其他两个两个请求都需要进行登录,按照控制台的密码进行登陆后会出现403的访问被禁止的相应。

image.png

3. 基于内存的多用户支持

目前为止都是只支持一个单用户登录,我们可以引入自定义UserDetailsService。

@Configuration
public class MyConfig {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("user").password(passwordEncoder.encode("123")).roles("USER").build());
        manager.createUser(User.withUsername("admin").password(passwordEncoder.encode("123")).roles("ADMIN", "USER").build());
        return manager;
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

在这里向容器当中注册一个UserDetailsService,在这里使用InMemoryUserDetailsManager,InMemoryUserDetailsManager是UserDetailsService的一个实现类,方便在内存中创建一个用户。

这里必须创建一个PasswordEncoder,否则在登陆的时候会报出java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"异常。
注意在角色名称必须和前面的一一对应,且区分大小写。

此时进行登录会发现全部按照我们预想的那样输出结果。

访问 /user/api 使用admin和user进行登录均可以进行访问,/admin/api只能登录admin才可以进行访问,否则出现403错误。

3.2 自定义数据库模型进行认证与授权

这里我们引入SpringDataJPA进行数据库的操作。
image.png

1. 资源准备

相关pom文件增加mysql的连接和jpa的连接

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <scope>runtime</scope>
</dependency>

在application.yml中配置数据库和jpa相关配置

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/spring?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
    username: root
    password: LY0115..

  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    open-in-view: false
    properties:
      hibernate:
        enable_lazy_load_no_trans: true

2. 创建UserEntity

实体类需要继承UserDetails,后续SpringSecurity会通过UserDetails进行后续的认证。主要是通过UserDetailsService的loadUserByUsername的方法获取一个UserDetails对象,该对象包含了用户的基本信息。

package cn.quguai.entity;

@Data
@Entity
@Table(name = "t_user")
public class UserEntity implements UserDetails {

    @Id
    @GeneratedValue
    private Long id;
    private String username;
    private String password;
    private Boolean enable;
    private String roles;

    @Transient
    private List<? extends GrantedAuthority> authorityList;

    public void setAuthorityList(List<GrantedAuthority> authorityList) {
        this.authorityList = authorityList;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorityList;
    }

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

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return this.enable;
    }
}

实现UserDetails定义的几个方法:

  • isAccountNonLocked、isCredentialsNonExpired、isAccountNonExpired暂且用不到,全部返回true,否则SpringSecurity会认为账号存在异常;
  • isEnabled对应enable字段,将其带入即可;
  • getAuthorities对应roles字段,由于结构不一致,这里新建一个,后续进行填充。

3. 创建UserRepository

后续需要操作数据库通过username获取UserEntity。

@Repository
public interface UserRepository extends JpaRepository<UserEntity, Long> {

    UserEntity findByUsername(String username);
}

4. 创建并实现UserDetailsService

需要删除事先创建的基于内存的UserDetailService

@Service
public class MyUserDetailService implements UserDetailsService {
    @Autowired
    private UserRepository userRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserEntity userEntity = userRepository.findByUsername(username);
        if (userEntity == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        userEntity.setPassword(passwordEncoder.encode(userEntity.getPassword()));
        userEntity.setAuthorityList(AuthorityUtils.commaSeparatedStringToAuthorityList(userEntity.getRoles()));
        return userEntity;
    }
}

由于前端密码都要经过SpringSecurity,为了能和数据库进行比较一致,所以需要将数据库中的密码也经过相同的算法进行加密。

由于数据库中存储的都是String的字符串,并以逗号进行分割,但是实体类中存储的却是GrantedAuthority,需要通过AuthorityUtils下的commaSeparatedStringToAuthorityList** 进行转换。**

5. 启动项目

在项目启动后,会自动创建数据库表,在数据库表中创建两个用户。方便后续的测试。

3.3 基于注解实现权限配置

1. 资源准备

修改WebSecurityConfig类,将原来的权限配置进行删除。

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.formLogin()
                .and().authorizeRequests()
                .antMatchers("/app/api").permitAll()
                //.antMatchers("/admin/api/**").hasRole("ADMIN")
                //.antMatchers("/user/api/**").hasRole("USER")
                .anyRequest().authenticated();
    }
}

2. 开启全局配置

@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
@SpringBootApplication
public class AuthenticationApplication {
    public static void main(String[] args) {
        SpringApplication.run(AuthenticationApplication.class, args);
    }
}

3. 使用注解

  • @Secured 可以传入一个String数组,代表拥有这些权限的都可以进行访问(如果是角色必须带上"ROLE_"的前缀)
  • @PreAuthorize可以传入一个函数进行判断是否包含这个角色:在方法执行之前进行判断
  • @PostAuthorize 和 @PreAuthorize类似只不过是在方法执行以后继续判断
@RestController
@RequestMapping("/user")
public class UserController {

    @PreAuthorize("hasRole('USER')")  //在方法之前进行验证
    @PostAuthorize("hasAuthority('USER')")  //在方法以后进行验证
    @Secured({"ROLE_USER"})
    @GetMapping("api")
    public String hello(){
        return "user Hello";
    }
}

Copyright: 采用 知识共享署名4.0 国际许可协议进行许可

Links: https://quguai.cn/archives/s-p-r-i-n-g--s-e-c-u-r-i-t-y--xue-xi-bi-ji--yi----ren-zheng-dao-shou-quan-cong-ru-men-dao-jing-tong