环境准备
controller
@RestController
@RequestMapping("/api/admin")
public class AdminController {
@GetMapping("/hello")
public String hello(){
return "hello! this is admin page";
}
}
/**----------------------分割线------------------------*/
@RestController
@RequestMapping("/api/user")
public class UserController {
@GetMapping("/hello")
public String hello(){
return "hello! this is user page!";
}
}
/**----------------------分割线------------------------*/
@RestController
@RequestMapping("/api/public")
public class PublicController {
@GetMapping("/hello")
public String hello(){
return "hello! this is public page";
}
}
资源权限配置
@EnableWebSecurity
public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/api/user/**").hasAnyRole("user") //user 角色访问/api/user/开头的路由
.antMatchers("/api/admin/**").hasAnyRole("admin") //admin角色访问/api/admin/开头的路由
.antMatchers("/api/public/**").permitAll() //允许所有可以访问/api/public/开头的路由
.and()
.formLogin();
}
}
antMatchers()
是一个采用 ANT 模式的 URL 匹配器:
*
表示匹配 0 或任意数量的字符**
表示匹配 0 或者更多的目录。antMatchers("/admin/api/**")
相当于匹配了/admin/api/
下的所有 API。
配置默认用户,密码和角色信息
#默认登录用户
spring.security.user.name=user
#默认user用户密码
spring.security.user.password=user
#默认user用户所属角色
spring.security.user.roles=user
启动服务进行验证
直接方访问localhost:8080/api/public/hello
, 没问题,正常访问:
访问localhost:8080/api/user/hello
, 跳转到了登录验证页面:
正确使用user
用户进行登录后, 也能够正常访问:
访问localhost:8080/api/admin/hello
, 跳转到了登录验证页面:
此时正确使用user
用户进行登录后,发现提示了403
:
页面显示
403
错误,表示该用户授权失败,401
代表该用户认证失败;本次访问已经通过了认证环节,只是在授权的时候被驳回了。
基于内存的多用户支持
到目前为止,我们仍然只有一个可登录的用户,怎样引入多用户呢?非常简单,我们只需实现一个自定义的UserDetailsService
即可:
@Bean
public UserDetailsService userDetailsService(){
UserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
//创建用户user01,密码user01,角色user
userDetailsManager.createUser(User.withUsername("user01").password("user01").roles("user").build());
//创建用户admin01,密码admin01,角色admin
userDetailsManager.createUser(User.withUsername("admin01").password("admin01").roles("admin").build());
return userDetailsManager;
}
其中InMemoryUserDetailsManager
是 UserDetailManager
的实现类,它将用户数据源寄存在内存里,在一些不需要引入数据库这种重数据源的系统中很有帮助。UserDetailManager
继承了 UserDetailService
;
重启服务,使用新创建的用户admin01
去访问localhost:8080/api/admin/hello
, 我们可以发现,依旧未能够正确认证并授权:
这块儿在陈木鑫老师的书中是已经能够正确认证并授权通过了,现在为什么没有成功呢? 因为 Spring security 5.0 中新增了多种加密方式,也改变了密码的格式。详见:https://blog.csdn.net/canon_in_d_major/article/details/79675033
我们这里针对性对userDetailsService
进行修改,指明使用默认的passwordEncoder
:
@Bean
public UserDetailsService userDetailsService(){
InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
//创建用户user01,密码user01,角色user
userDetailsManager.createUser(User.withDefaultPasswordEncoder().username("user01").password("user01").roles("user").build());
//创建用户admin01,密码admin01,角色admin
userDetailsManager.createUser(User.withDefaultPasswordEncoder().username("admin01").password("admin01").roles("admin").build());
return userDetailsManager;
}
这样我们就可以正确的认证访问了:
基于默认数据库模型的认证与授权
除了InMemoryUserDetailsManager
,Spring Security 还提供另一个UserDetailsService
实现类:JdbcUserDetailsManager
。JdbcUserDetailsManager
帮助我们以JDBC
的方式对接数据库和 Spring Security。
JdbcUserDetailsManager
设定了一个默认的数据库模型,只要遵从这个模型,在简便性上,JdbcUserDetailsManager
甚至可以媲美InMemoryUserDetailsManager
。
准备数据库
我这里使用的是PostgreSQL
数据库,在这块儿使用其他数据库(例如MySQL
)都是一样的,这块儿看个人爱好吧。
(1)在工程中引入jdbc
和PostgreSQL
两个必要依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
(2)在application.properties
配置文件中配置数据库连接信息:
#数据库连接信息
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localhost:5432/springsecuritydemo?schema=public
spring.datasource.data-username=postgres
spring.datasource.data-password=aaaaaa
(3)预制表结构和数据JdbcUserDetailsManager
设定了一个默认的数据库模型,Spring Security 将该模型定义在/org/springframework/security/core/userdetails/jdbc/users.ddl
内:
create table users(username varchar_ignorecase(50) not null primary key,password varchar_ignorecase(500) not null,enabled boolean not null);
create table authorities (username varchar_ignorecase(50) not null,authority varchar_ignorecase(50) not null,constraint fk_authorities_users foreign key(username) references users(username));
create unique index ix_auth_username on authorities (username,authority);
JdbcUserDetailsManager
需要两个表,其中users
表用来存放用户名、密码和是否可用三个信息,authorities
表用来存放用户名及其权限的对应关系。
我们现在使用 sql 创建这两张表,在 Pg 数据库中执行上面的语句,我们发现以下错误:
因为该语句是用hsqldb
创建的,而 PostgreSQL 不支持varchar_ignorecase
这种类型。怎么办呢?很简单,将varchar_ignorecase
改为 PostgreSQL 支持的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));
create unique index ix_auth_username on authorities (username,authority);
配置完成后,重启环境,正常启动。
编码实现
下面我们修改一下userDetailService
bean, 使用JdbcUserDetailsManager
实现,让 Spring Security 使用数据库来管理用户:
@Bean
public UserDetailsService userDetailsService(DataSource dataSource){
JdbcUserDetailsManager userDetailsManager = new JdbcUserDetailsManager();
userDetailsManager.setDataSource(dataSource);
//创建用户user01,密码user01,角色user
userDetailsManager.createUser(User.withDefaultPasswordEncoder().username("user01").password("user01").roles("user").build());
//创建用户admin01,密码admin01,角色admin
userDetailsManager.createUser(User.withDefaultPasswordEncoder().username("admin01").password("admin01").roles("admin").build());
return userDetailsManager;
}
JdbcUserDetailsManager
与InMemoryUserDetailsManager
在用法上没有太大区别,只是多了设置DataSource
的环节。Spring Security 通过DataSource
执行设定好的命令。例如,此处的createUser
函数实际上就是执行了下面的 SQL 语句:
insert into users (username,password,enabled) values(?,?,?)
查看 JdbcUserDetailsManager
的源代码可以看到更多定义好的 SQL 语句,诸如deleteUserSql
、updateUserSql
等,这些都是JdbcUserDetailsManager
与数据库实际交互的形式。当然,JdbcUserDetailsManager
也允许我们在特殊情况下自定义这些 SQL 语句,如有必要,调用对应的setXxxSql
方法即可。
现在重启服务,我们发现看看 Spring Security 在数据库中生成了下面这些数据:
users 表:
authorities 表:
重启服务后,使用user01
用户和admin01
用户都能够正常合理访问接口,与预期的行为一致。
到目前为止,一切都工作得很好,但是只要我们重启服务,应用就会报错。这是因为 users 表在创建语句时,username 字段为主键,主键是唯一不重复的,但重启服务后会再次创建 admin 和 user,导致数据库报错(在内存数据源上不会出现这种问题,因为重启服务后会清空 username 字段中的内容)。
所以如果需要在服务启动时便生成部分用户,那么建议先判断用户名是否存在。
@Bean
public UserDetailsService userDetailsService(DataSource dataSource){
JdbcUserDetailsManager userDetailsManager = new JdbcUserDetailsManager();
userDetailsManager.setDataSource(dataSource);
//创建用户user01,密码user01,角色user
if (!userDetailsManager.userExists("user01")) { //判断user01是否存在
userDetailsManager.createUser(User.withDefaultPasswordEncoder().username("user01").password("user01").roles("user").build());
}
//创建用户admin01,密码admin01,角色admin
if (!userDetailsManager.userExists("admin01")) {//判断admin01是否存在
userDetailsManager.createUser(User.withDefaultPasswordEncoder().username("admin01").password("admin01").roles("admin").build());
}
return userDetailsManager;
}
补充
WebSecurityConfigurer Adapter
定义了三个configure
:
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
this.disableLocalConfigureAuthenticationBldr = true;
}
public void configure(WebSecurity web) throws Exception {
}
protected void configure(HttpSecurity http) throws Exception {
this.logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");
((HttpSecurity)((HttpSecurity)((AuthorizedUrl)http.authorizeRequests().anyRequest()).authenticated().and()).formLogin().and()).httpBasic();
}
我们只用到了一个参数,用来接收 HttpSecurity 对象的配置方法。另外两个参数也有各自的用途,其中,AuthenticationManagerBuilder的configure
同样允许我们配置认证用户:
@EnableWebSecurity
public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/api/user/**").hasAnyRole("user") //user 角色访问/api/user/开头的路由
.antMatchers("/api/admin/**").hasAnyRole("admin") //admin 角色访问/api/admin/开头的路由
.antMatchers("/api/public/**").permitAll() //允许所有可以访问/api/public/开头的路由
.and()
.formLogin();
}
// @Bean
// public UserDetailsService userDetailsService(){
// InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
//
// //创建用户user01,密码user01,角色user
// userDetailsManager.createUser(User.withDefaultPasswordEncoder().username("user01").password("user01").roles("user").build());
// //创建用户admin01,密码admin01,角色admin
// userDetailsManager.createUser(User.withDefaultPasswordEncoder().username("admin01").password("admin01").roles("admin").build());
//
// return userDetailsManager;
// }
// @Bean
// public UserDetailsService userDetailsService(DataSource dataSource){
// JdbcUserDetailsManager userDetailsManager = new JdbcUserDetailsManager();
// userDetailsManager.setDataSource(dataSource);
//
// //创建用户user01,密码user01,角色user
// if (!userDetailsManager.userExists("user01")) { //判断user01是否存在
// userDetailsManager.createUser(User.withDefaultPasswordEncoder().username("user01").password("user01").roles("user").build());
// }
// //创建用户admin01,密码admin01,角色admin
// if (!userDetailsManager.userExists("admin01")) {//判断admin01是否存在
// userDetailsManager.createUser(User.withDefaultPasswordEncoder().username("admin01").password("admin01").roles("admin").build());
// }
// return userDetailsManager;
// }
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication()
.passwordEncoder(new BCryptPasswordEncoder())
.withUser("user1")
.password(new BCryptPasswordEncoder().encode("user01"))
.roles("user")
.and()
.passwordEncoder(new BCryptPasswordEncoder())
.withUser("admin01")
.password(new BCryptPasswordEncoder().encode("admin01"))
.roles("admin");
}
}
自定义数据库模型的认证于授权
InMemoryUserDetailsManager
和 JdbcUserDetailsManager
两个类都是UserDetailsService
的实现类,自定义数据库结构实际上也仅需实现一个自定义的UserDetailsService
。UserDetailsService
仅定义了一个loadUserByUsername
方法,用于获取一个UserDetails
对象。UserDetails
对象包含了一系列在验证时会用到的信息,包括用户名、密码、权限以及其他信息,Spring Security 会根据这些信息判定验证是否成功。
public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
也就是说,不管数据库结构如何变化,只要能构造一个UserDetails
即可。
自定义实现UserDetail
- 编写实体
User
实现UserDetail
public class User implements UserDetails {
private Long id;
private String userName;
private String password;
private Boolean enable;
private String roles;
private List<GrantedAuthority> authentications;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public void setPassword(String password) {
this.password = password;
}
public String getRoles() {
return roles;
}
public void setRoles(String roles) {
this.roles = roles;
}
public Boolean getEnable() {
return enable;
}
public void setEnable(Boolean enable) {
this.enable = enable;
}
public List<GrantedAuthority> getAuthentications() {
return authentications;
}
public void setAuthentications(List<GrantedAuthority> authentications) {
this.authentications = authentications;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.getAuthentications();
}
@Override
public String getPassword() {
return this.password;
}
@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
定义的几个方法:
isAccountNonExpired
、isAccountNonLocked
和isCredentialsNonExpired
暂且用不到,统一返回rue
,否则 Spring Security 会认为账号异常。isEnabled
对应enable
字段,将其代入即可。getAuthorities
方法本身对应的是roles
字段,但由于结构不一致,所以此处新建一个,并在后续进行填充。
- 数据库持久层
这里使用JPA
实现实体关系型映射,建立实体与数据库的关系:
(1)需要引入spring-boot-stater-data-jpa
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
(2)更改User
为数据库映射实体类:
@Entity
@Table
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
private String userName;
private String password;
private Boolean enable;
private String roles;
@Transient
private List<GrantedAuthority> authentications;
....
....
(3)新建UserRepository
public interface UserRepository extends JpaRepository<User,Long> {
User findByUserName(String userName);
}
自定义实现UserDetailsService
@Service
public class MyUserDetailServiceImpl implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//(1)从数据库获取用户
User user = userRepository.findByUserName(username);
if (user==null)//用户不存在
throw new RuntimeException("用户"+username+"不存在!");
//(2)将数据库中的roles解析为UserDetail的权限集
String roles = user.getRoles();
List<GrantedAuthority> grantedAuthorities = AuthorityUtils.commaSeparatedStringToAuthorityList(roles);
user.setAuthentications(grantedAuthorities);
return user;
}
}
AuthorityUtils.commaSeparatedStringToAuthorityList(String list)
是 spring security 提供的将逗号隔开的权限集字符串切割为权限对象列表,当然上面代码中我们也可以自己实现来代替:
List<GrantedAuthority> getGrantedAuthorities(String roles){
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
String[] split = StringUtils.split(roles, ";");
for (int i = 0;i<split.length;i++){
if (!StringUtils.isEmpty(split[i])){
SimpleGrantedAuthority grantedAuthority = new SimpleGrantedAuthority(split[i]);
grantedAuthorities.add(grantedAuthority);
}
}
return grantedAuthorities;
}
至此,我们就实现了 Spring Security 的自定义数据库结构认证工程。