Spring Security教程 第一弹 初识spring security

商同
2023-12-01

写在前面的话

更多Spring与微服务相关的教程请戳这里 Spring与微服务教程合集

1、概述

核心概念:

  • 认证
  • 授权:Spring Security不仅支持基于URL对Web的请求授权,还支持方法访问授权、对象访问
    授权等

Spring Security已经集成的认证技术如下:

  • HTTP BASIC authentication headers:一个基于IETF RFC的标准
  • HTTP Digest authentication headers:一个基于IETF RFC的标准
  • HTTP X.509 client certificate exchange:一个基于IETF RFC的标准
  • LDAP:一种常见的跨平台身份验证方式
  • Form-based authentication:用于简单的用户界面需求
  • OpenID authentication:一种去中心化的身份认证方式
  • Authentication based on pre-established request headers:类似于 Computer Associates SiteMinder,一种用户身份验证及授权的集中式安全基础方案
  • Jasig Central Authentication Service:单点登录方案
  • Transparent authentication context propagation for Remote Method Invocation(RMI)and HttpInvoker:一个Spring远程调用协议
  • Automatic "remember-me" authentication:允许在指定到期时间前自行重新登录系统
  • Anonymous authentication:允许匿名用户使用特定的身份安全访问资源
  • Run-as authentication:允许在一个会话中变换用户身份的机制
  • Java Authentication and Authorization Service:JAAS,Java验证和授权API
  • Java EE container authentication:允许系统继续使用容器管理这种身份验证方式
  • Kerberos:一种使用对称密钥机制,允许客户端与服务器相互确认身份的认证协议

除此之外,Spring Security还引入了一些第三方包,用于支持更多的认证技术,如JOSSO等。

如果所有这些技术都无法满足需求,则Spring Security允许我们编写自己的认证技术

2、入门案例

2.1、pom.xml

<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

2.2、application.yml

spring:
  security:
    user:
      #如果不配置,则默认的用户名为user,默认密码则在控制台打印
      name: admin
      password: ok
server:
  port: 8140
  servlet:
    context-path: /spring-security-base

2.3、启动类

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@SpringBootApplication
@RestController
public class SpringSecurityBaseApplication {
    @RequestMapping("hello")
    public String hello(){
        return "hello!";
    }


    public static void main(String[] args) {
        SpringApplication.run(SpringSecurityBaseApplication.class, args);
    }

}

注意:

  • 项目引入spring security之后,虽然不做任何配置,也会有一个Http基本认证(由于版本不同,也可能是表单认证)
  • 默认用户名是user,密码会在控制台打印;也可以在application.yml中配置用户名密码

3、EnableWebSecurity自动配置

3.1、开启自动配置

如果不重写configure(HttpSecurity http)方法,则默认会有以下行为:

  • 验证所有请求
  • 允许表单验证
  • 允许Http基本认证

     

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
    }
}

3.2、HttpSecurity对象

HttpSecurity被设计为链式调用,且实际上对应的是XML中的标签

HttpSecurity提供了很多配置相关的方法,分别对应命名空间配置中的子标签<http>。

例如:

  • authorizeRequests()     对应    <intercept-url>
  • formLogin()                  对应    <formlogin>
  • httpBasic()                  对应    <http-basic>
  • csrf()                           对应    <csrf>

调用这些方法之后,除非使用and()方法结束当前标签,上下文才会回到HttpSecurity,否则链式调用的上下文将自动进入对应标签域。

  • authorizeRequests()方法实际上返回了一个 URL 拦截注册器,我们可以调用它提供的anyanyRequest()、antMatchers()和regexMatchers()等方法来匹配系统的URL,并为其指定安全策略
  • formLogin()方法和httpBasic()方法都声明了认证方式
  • csrf()方法是Spring Security提供的跨站请求伪造防护功能,当我们继承WebSecurityConfigurerAdapter时会默认开启csrf()方法

4、认证和授权

4.1、基于内存

4.1.1、准备controller

准备三个controller,分别是AdminController、UserController、AppController,每个controller里面都提供hello方法

/admin/hello需要ADMIN角色才能访问,/user/hello需要USER角色才能访问,/app/hello不需要登录就能访问,为开放资源

@RestController
@RequestMapping("/admin")
public class AdminController {
    @RequestMapping("/hello")
    public String hello(){
        return "hello,admin!";
    }
}


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

    @RequestMapping("/hello")
    public String hello(){
        return "hello,user!";
    }
}
@RestController
@RequestMapping("/app")
public class AppController {

    @RequestMapping("/hello")
    public String hello(){
        return "hello,app!";
    }
}

4.1.2、WebSecurityConfig

Spring Security支持各种来源的用户数据,包括内存、数据库、LDAP等。它们被抽象为一个UserDetailsService接口,
任何实现了UserDetailsService 接口的对象都可以作为认证数据源

下面配置了来源于内存的用户,并为用户分配角色,且spring security 5之后密码必须进行加密,否则登录时会报错
 

import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/user/**").hasRole("USER")
                .antMatchers("/app/**").permitAll()
                .anyRequest().authenticated()
                .and().formLogin()
                .and().csrf().disable();
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        String pwd = encoder.encode("ok");

        auth.inMemoryAuthentication().passwordEncoder(encoder)
        .withUser("bobo").password(pwd).roles("ADMIN")
        .and()
        .withUser("tudou").password(pwd).roles("USER");
    }
}

4.2、基于默认数据库

4.2.1、初始化数据库

除了InMemoryUserDetailsManager,Spring Security还提供另一个UserDetailsService实现类:JdbcUserDetailsManager
JdbcUserDetailsManager帮助我们以JDBC的方式对接数据库和Spring Security,它设定了一个默认的数据库模型

默认数据库模型脚本在/org/springframework/security/core/userdetails/jdbc/users.ddl中

JdbcUserDetailsManager需要两个表,其中users表用来存放用户名、密码和是否可用三个信息,authorities表用来存放用户名及其权限的对应关系。

该语句是用hsqldb创建的,而MySQL不支持varchar_ignorecase这种类型,将varchar_ignorecase改为MySQL支持的varchar即可

create table users(
  username varchar(50) not null primary key,
  password varchar(500) not null,
  enabled boolean not null
);
create table authorities (
  username varchar(50) not null,
  authority varchar(50) not null,
  constraint fk_authorities_users foreign key(username) references users(username)
);

4.2.2、pom.xml

<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-jdbc</artifactId>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>8.0.15</version>
    </dependency>
  </dependencies>

4.2.3、application.yml

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/spring-security-base?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
    username: root
    password: ok

4.2.4、WebSecurityConfig

基于内存和基于jdbc两种方式没太大区别,只是基于jdbc会多一步设置数据源的操作

JdbcUserDetailsManager封装了操作数据库的细节,比如createUser方法实际上对应insert into user

项目启动时,会向数据库创建用户,但再次启动就会报错,因为数据库中已经有这两个用户了,解决办法是创建用户之前先判断该用户是否存在;但使用内存方式就不会出现重启问题,因为每次重启内存中的用户信息会清空

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import javax.sql.DataSource;

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/user/**").hasRole("USER")
                .antMatchers("/app/**").permitAll()
                .anyRequest().authenticated()
                .and().formLogin()
                .and().csrf().disable();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        String pwd = encoder.encode("ok");

        JdbcUserDetailsManagerConfigurer<AuthenticationManagerBuilder> configurer =
                auth.jdbcAuthentication().dataSource(dataSource).passwordEncoder(encoder);
        if(!configurer.getUserDetailsService().userExists("bobo")){
            configurer.withUser("bobo").password(pwd).roles("ADMIN");
        }
        if(!configurer.getUserDetailsService().userExists("tudou")){
            configurer.withUser("tudou").password(pwd).roles("USER");
        }
    }
}

4.3、基于自定义数据库

自定义数据库结构实际上是实现一个自定义的UserDetailsService

4.3.1、自定义数据库模型

CREATE TABLE sys_user (
  id int(10) unsigned NOT NULL AUTO_INCREMENT,
  username varchar(255) DEFAULT NULL,
  roles varchar(255) DEFAULT NULL,
  password varchar(255) DEFAULT NULL,
  PRIMARY KEY (id)
);


INSERT INTO sys_user(id, username, roles, password) VALUES (1, 'test1', 'ROLE_ADMIN,ROLE_USER', '789');
INSERT INTO sys_user(id, username, roles, password) VALUES (2, 'test2', 'ROLE_USER', '123');

4.3.2、SysUser类

SysUser只是单纯地与数据库交互,不提供身份认证和权限认证能力

public class SysUser {

    private Integer id;
    private String username;
    private String password;
    //逗号分隔的角色
    private String roles;

    //省略get/set方法
}

4.3.3、SecurityUser类

SecurityUser类继承SysUser类,且实现UserDetails接口,该接口提供身份认证和权限认证能力

UserDetail接口源码:

package org.springframework.security.core.userdetails;

import java.io.Serializable;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;

public interface UserDetails extends Serializable {
    //获取权限
    Collection<? extends GrantedAuthority> getAuthorities();

    String getPassword();

    String getUsername();

    //下面四个方法均返回true就行

    boolean isAccountNonExpired();

    boolean isAccountNonLocked();

    boolean isCredentialsNonExpired();

    boolean isEnabled();
}

SecurityUser类:

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;

public class SecurityUser extends SysUser implements UserDetails {

    public SecurityUser(){}
    public SecurityUser(SysUser sysUser){
        super.setId(sysUser.getId());
        super.setUsername(sysUser.getUsername());
        super.setPassword(sysUser.getPassword());
        super.setRoles(sysUser.getRoles());
    }

    //spring security用于存放权限的属性
    private List<GrantedAuthority> authorityList;

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

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

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

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

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

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

4.3.4、自定义UserDetailsService

ISysUserMapper采用mybatis实现:

import com.bobo.group.springsecuritybase.entity.SysUser;
import org.apache.ibatis.annotations.Select;

public interface ISysUserMapper {

    @Select("select * from sys_user where username=#{username}")
    SysUser getUserByUsername(String username);
}

CustomUserDetailService:

import com.bobo.group.springsecuritybase.dao.ISysUserMapper;
import com.bobo.group.springsecuritybase.entity.SecurityUser;
import com.bobo.group.springsecuritybase.entity.SysUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class CustomUserDetailService implements UserDetailsService {
    @Autowired
    private ISysUserMapper iSysUserMapper;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        SysUser sysUser = iSysUserMapper.getUserByUsername(s);
        if(null == sysUser){
            throw new UsernameNotFoundException("该用户不存在!");
        }

        //将数据库形式的roles转换为UserDetails识别的权限集
        //commaSeparatedStringToAuthorityList方法的实现是按逗号分隔,我们也可以借助SimpleGrantedAuthority类提供其它实现
        List<GrantedAuthority> list = AuthorityUtils.commaSeparatedStringToAuthorityList(sysUser.getRoles());
        SecurityUser securityUser = new SecurityUser(sysUser);
        securityUser.setAuthorityList(list);
        return securityUser;
    }
}

4.3.5、WebSecurityConfig

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 提供不加密的PasswordEncoder
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    @Autowired
    private CustomUserDetailService customUserDetailService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/user/**").hasRole("USER")
                .antMatchers("/app/**").permitAll()
                .anyRequest().authenticated()
                .and().formLogin()
                .and().csrf().disable();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //基于自定义userDetailsService
        //这个一定要设置一下,否则就会报No AuthenticationProvider found的错
        auth.userDetailsService(customUserDetailService);
    }
}

 类似资料: