写在前面的话

更多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);
}
}