写在前面的话
更多Spring与微服务相关的教程请戳这里 Spring与微服务教程合集
核心概念:
Spring Security已经集成的认证技术如下:
除此之外,Spring Security还引入了一些第三方包,用于支持更多的认证技术,如JOSSO等。
如果所有这些技术都无法满足需求,则Spring Security允许我们编写自己的认证技术
<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>
spring:
security:
user:
#如果不配置,则默认的用户名为user,默认密码则在控制台打印
name: admin
password: ok
server:
port: 8140
servlet:
context-path: /spring-security-base
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);
}
}
注意:
如果不重写configure(HttpSecurity 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);
}
}
HttpSecurity被设计为链式调用,且实际上对应的是XML中的标签
HttpSecurity提供了很多配置相关的方法,分别对应命名空间配置中的子标签<http>。
例如:
调用这些方法之后,除非使用and()方法结束当前标签,上下文才会回到HttpSecurity,否则链式调用的上下文将自动进入对应标签域。
准备三个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!";
}
}
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");
}
}
除了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)
);
<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>
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
基于内存和基于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");
}
}
}
自定义数据库结构实际上是实现一个自定义的UserDetailsService
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');
SysUser只是单纯地与数据库交互,不提供身份认证和权限认证能力
public class SysUser {
private Integer id;
private String username;
private String password;
//逗号分隔的角色
private String roles;
//省略get/set方法
}
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;
}
}
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;
}
}
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);
}
}