环境准备

controller

  1. @RestController
  2. @RequestMapping("/api/admin")
  3. public class AdminController {
  4. @GetMapping("/hello")
  5. public String hello(){
  6. return "hello! this is admin page";
  7. }
  8. }
  9. /**----------------------分割线------------------------*/
  10. @RestController
  11. @RequestMapping("/api/user")
  12. public class UserController {
  13. @GetMapping("/hello")
  14. public String hello(){
  15. return "hello! this is user page!";
  16. }
  17. }
  18. /**----------------------分割线------------------------*/
  19. @RestController
  20. @RequestMapping("/api/public")
  21. public class PublicController {
  22. @GetMapping("/hello")
  23. public String hello(){
  24. return "hello! this is public page";
  25. }
  26. }

资源权限配置

  1. @EnableWebSecurity
  2. public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {
  3. @Override
  4. protected void configure(HttpSecurity http) throws Exception {
  5. http.authorizeRequests()
  6. .antMatchers("/api/user/**").hasAnyRole("user") //user 角色访问/api/user/开头的路由
  7. .antMatchers("/api/admin/**").hasAnyRole("admin") //admin角色访问/api/admin/开头的路由
  8. .antMatchers("/api/public/**").permitAll() //允许所有可以访问/api/public/开头的路由
  9. .and()
  10. .formLogin();
  11. }
  12. }

antMatchers()是一个采用 ANT 模式的 URL 匹配器:

  • * 表示匹配 0 或任意数量的字符
  • ** 表示匹配 0 或者更多的目录。antMatchers("/admin/api/**")相当于匹配了/admin/api/下的所有 API。

配置默认用户,密码和角色信息

  1. #默认登录用户
  2. spring.security.user.name=user
  3. #默认user用户密码
  4. spring.security.user.password=user
  5. #默认user用户所属角色
  6. spring.security.user.roles=user

启动服务进行验证

直接方访问localhost:8080/api/public/hello, 没问题,正常访问:
(二)基于数据库的认证与授权 - 图1
访问localhost:8080/api/user/hello, 跳转到了登录验证页面:
(二)基于数据库的认证与授权 - 图2
正确使用user用户进行登录后, 也能够正常访问:
(二)基于数据库的认证与授权 - 图3
访问localhost:8080/api/admin/hello, 跳转到了登录验证页面:

此时正确使用user用户进行登录后,发现提示了403
(二)基于数据库的认证与授权 - 图4

页面显示403错误,表示该用户授权失败,401代表该用户认证失败;本次访问已经通过了认证环节,只是在授权的时候被驳回了。

基于内存的多用户支持

到目前为止,我们仍然只有一个可登录的用户,怎样引入多用户呢?非常简单,我们只需实现一个自定义的UserDetailsService即可:

  1. @Bean
  2. public UserDetailsService userDetailsService(){
  3. UserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
  4. //创建用户user01,密码user01,角色user
  5. userDetailsManager.createUser(User.withUsername("user01").password("user01").roles("user").build());
  6. //创建用户admin01,密码admin01,角色admin
  7. userDetailsManager.createUser(User.withUsername("admin01").password("admin01").roles("admin").build());
  8. return userDetailsManager;
  9. }

其中InMemoryUserDetailsManagerUserDetailManager的实现类,它将用户数据源寄存在内存里,在一些不需要引入数据库这种重数据源的系统中很有帮助。
UserDetailManager 继承了 UserDetailService;

重启服务,使用新创建的用户admin01去访问localhost:8080/api/admin/hello, 我们可以发现,依旧未能够正确认证并授权:
(二)基于数据库的认证与授权 - 图5

这块儿在陈木鑫老师的书中是已经能够正确认证并授权通过了,现在为什么没有成功呢? 因为 Spring security 5.0 中新增了多种加密方式,也改变了密码的格式。详见:https://blog.csdn.net/canon_in_d_major/article/details/79675033

我们这里针对性对userDetailsService进行修改,指明使用默认的passwordEncoder

  1. @Bean
  2. public UserDetailsService userDetailsService(){
  3. InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
  4. //创建用户user01,密码user01,角色user
  5. userDetailsManager.createUser(User.withDefaultPasswordEncoder().username("user01").password("user01").roles("user").build());
  6. //创建用户admin01,密码admin01,角色admin
  7. userDetailsManager.createUser(User.withDefaultPasswordEncoder().username("admin01").password("admin01").roles("admin").build());
  8. return userDetailsManager;
  9. }

这样我们就可以正确的认证访问了:
(二)基于数据库的认证与授权 - 图6

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

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

JdbcUserDetailsManager设定了一个默认的数据库模型,只要遵从这个模型,在简便性上,JdbcUserDetailsManager甚至可以媲美InMemoryUserDetailsManager

准备数据库

我这里使用的是PostgreSQL数据库,在这块儿使用其他数据库(例如MySQL)都是一样的,这块儿看个人爱好吧。

(1)在工程中引入jdbcPostgreSQL两个必要依赖:

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-jdbc</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>org.postgresql</groupId>
  7. <artifactId>postgresql</artifactId>
  8. </dependency>

(2)在application.properties配置文件中配置数据库连接信息:

  1. #数据库连接信息
  2. spring.datasource.driver-class-name=org.postgresql.Driver
  3. spring.datasource.url=jdbc:postgresql://localhost:5432/springsecuritydemo?schema=public
  4. spring.datasource.data-username=postgres
  5. spring.datasource.data-password=aaaaaa

(3)预制表结构和数据
JdbcUserDetailsManager设定了一个默认的数据库模型,Spring Security 将该模型定义在/org/springframework/security/core/userdetails/jdbc/users.ddl内:

  1. create table users(username varchar_ignorecase(50) not null primary key,password varchar_ignorecase(500) not null,enabled boolean not null);
  2. 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));
  3. create unique index ix_auth_username on authorities (username,authority);

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

我们现在使用 sql 创建这两张表,在 Pg 数据库中执行上面的语句,我们发现以下错误:
(二)基于数据库的认证与授权 - 图7
因为该语句是用hsqldb创建的,而 PostgreSQL 不支持
varchar_ignorecase这种类型。怎么办呢?很简单,将varchar_ignorecase改为 PostgreSQL 支持的varchar即可:

  1. create table users(username varchar(50) not null primary key,password varchar(500) not null,enabled boolean not null);
  2. create table authorities (username varchar(50) not null,authority varchar(50) not null,constraint fk_authorities_users foreign key(username) references users(username));
  3. create unique index ix_auth_username on authorities (username,authority);

(二)基于数据库的认证与授权 - 图8
(二)基于数据库的认证与授权 - 图9
(二)基于数据库的认证与授权 - 图10
配置完成后,重启环境,正常启动。

编码实现

下面我们修改一下userDetailService bean, 使用JdbcUserDetailsManager实现,让 Spring Security 使用数据库来管理用户:

  1. @Bean
  2. public UserDetailsService userDetailsService(DataSource dataSource){
  3. JdbcUserDetailsManager userDetailsManager = new JdbcUserDetailsManager();
  4. userDetailsManager.setDataSource(dataSource);
  5. //创建用户user01,密码user01,角色user
  6. userDetailsManager.createUser(User.withDefaultPasswordEncoder().username("user01").password("user01").roles("user").build());
  7. //创建用户admin01,密码admin01,角色admin
  8. userDetailsManager.createUser(User.withDefaultPasswordEncoder().username("admin01").password("admin01").roles("admin").build());
  9. return userDetailsManager;
  10. }

JdbcUserDetailsManagerInMemoryUserDetailsManager在用法上没有太大区别,只是多了设置DataSource的环节。Spring Security 通过DataSource执行设定好的命令。例如,此处的createUser函数实际上就是执行了下面的 SQL 语句:

  1. insert into users (username,password,enabled) values(?,?,?)

查看 JdbcUserDetailsManager 的源代码可以看到更多定义好的 SQL 语句,诸如deleteUserSqlupdateUserSql等,这些都是JdbcUserDetailsManager与数据库实际交互的形式。当然,JdbcUserDetailsManager 也允许我们在特殊情况下自定义这些 SQL 语句,如有必要,调用对应的setXxxSql方法即可。
(二)基于数据库的认证与授权 - 图11
现在重启服务,我们发现看看 Spring Security 在数据库中生成了下面这些数据:
users 表:
(二)基于数据库的认证与授权 - 图12
authorities 表:
(二)基于数据库的认证与授权 - 图13
重启服务后,使用user01用户和admin01用户都能够正常合理访问接口,与预期的行为一致。

到目前为止,一切都工作得很好,但是只要我们重启服务,应用就会报错。这是因为 users 表在创建语句时,username 字段为主键,主键是唯一不重复的,但重启服务后会再次创建 admin 和 user,导致数据库报错(在内存数据源上不会出现这种问题,因为重启服务后会清空 username 字段中的内容)。
所以如果需要在服务启动时便生成部分用户,那么建议先判断用户名是否存在。

  1. @Bean
  2. public UserDetailsService userDetailsService(DataSource dataSource){
  3. JdbcUserDetailsManager userDetailsManager = new JdbcUserDetailsManager();
  4. userDetailsManager.setDataSource(dataSource);
  5. //创建用户user01,密码user01,角色user
  6. if (!userDetailsManager.userExists("user01")) { //判断user01是否存在
  7. userDetailsManager.createUser(User.withDefaultPasswordEncoder().username("user01").password("user01").roles("user").build());
  8. }
  9. //创建用户admin01,密码admin01,角色admin
  10. if (!userDetailsManager.userExists("admin01")) {//判断admin01是否存在
  11. userDetailsManager.createUser(User.withDefaultPasswordEncoder().username("admin01").password("admin01").roles("admin").build());
  12. }
  13. return userDetailsManager;
  14. }

补充

WebSecurityConfigurer Adapter定义了三个configure:

  1. protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  2. this.disableLocalConfigureAuthenticationBldr = true;
  3. }
  4. public void configure(WebSecurity web) throws Exception {
  5. }
  6. protected void configure(HttpSecurity http) throws Exception {
  7. this.logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");
  8. ((HttpSecurity)((HttpSecurity)((AuthorizedUrl)http.authorizeRequests().anyRequest()).authenticated().and()).formLogin().and()).httpBasic();
  9. }

我们只用到了一个参数,用来接收 HttpSecurity 对象的配置方法。另外两个参数也有各自的用途,其中,AuthenticationManagerBuilder的configure同样允许我们配置认证用户:

  1. @EnableWebSecurity
  2. public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {
  3. @Override
  4. protected void configure(HttpSecurity http) throws Exception {
  5. http.authorizeRequests()
  6. .antMatchers("/api/user/**").hasAnyRole("user") //user 角色访问/api/user/开头的路由
  7. .antMatchers("/api/admin/**").hasAnyRole("admin") //admin 角色访问/api/admin/开头的路由
  8. .antMatchers("/api/public/**").permitAll() //允许所有可以访问/api/public/开头的路由
  9. .and()
  10. .formLogin();
  11. }
  12. // @Bean
  13. // public UserDetailsService userDetailsService(){
  14. // InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
  15. //
  16. // //创建用户user01,密码user01,角色user
  17. // userDetailsManager.createUser(User.withDefaultPasswordEncoder().username("user01").password("user01").roles("user").build());
  18. // //创建用户admin01,密码admin01,角色admin
  19. // userDetailsManager.createUser(User.withDefaultPasswordEncoder().username("admin01").password("admin01").roles("admin").build());
  20. //
  21. // return userDetailsManager;
  22. // }
  23. // @Bean
  24. // public UserDetailsService userDetailsService(DataSource dataSource){
  25. // JdbcUserDetailsManager userDetailsManager = new JdbcUserDetailsManager();
  26. // userDetailsManager.setDataSource(dataSource);
  27. //
  28. // //创建用户user01,密码user01,角色user
  29. // if (!userDetailsManager.userExists("user01")) { //判断user01是否存在
  30. // userDetailsManager.createUser(User.withDefaultPasswordEncoder().username("user01").password("user01").roles("user").build());
  31. // }
  32. // //创建用户admin01,密码admin01,角色admin
  33. // if (!userDetailsManager.userExists("admin01")) {//判断admin01是否存在
  34. // userDetailsManager.createUser(User.withDefaultPasswordEncoder().username("admin01").password("admin01").roles("admin").build());
  35. // }
  36. // return userDetailsManager;
  37. // }
  38. @Override
  39. protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  40. auth.jdbcAuthentication()
  41. .passwordEncoder(new BCryptPasswordEncoder())
  42. .withUser("user1")
  43. .password(new BCryptPasswordEncoder().encode("user01"))
  44. .roles("user")
  45. .and()
  46. .passwordEncoder(new BCryptPasswordEncoder())
  47. .withUser("admin01")
  48. .password(new BCryptPasswordEncoder().encode("admin01"))
  49. .roles("admin");
  50. }
  51. }

自定义数据库模型的认证于授权

InMemoryUserDetailsManagerJdbcUserDetailsManager两个类都是UserDetailsService的实现类,自定义数据库结构实际上也仅需实现一个自定义的UserDetailsService
UserDetailsService仅定义了一个loadUserByUsername方法,用于获取一个UserDetails对象。UserDetails对象包含了一系列在验证时会用到的信息,包括用户名、密码、权限以及其他信息,Spring Security 会根据这些信息判定验证是否成功。

  1. public interface UserDetailsService {
  2. UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
  3. }
  1. public interface UserDetails extends Serializable {
  2. Collection<? extends GrantedAuthority> getAuthorities();
  3. String getPassword();
  4. String getUsername();
  5. boolean isAccountNonExpired();
  6. boolean isAccountNonLocked();
  7. boolean isCredentialsNonExpired();
  8. boolean isEnabled();
  9. }

也就是说,不管数据库结构如何变化,只要能构造一个UserDetails即可。

自定义实现UserDetail

  1. 编写实体User实现UserDetail
  1. public class User implements UserDetails {
  2. private Long id;
  3. private String userName;
  4. private String password;
  5. private Boolean enable;
  6. private String roles;
  7. private List<GrantedAuthority> authentications;
  8. public Long getId() {
  9. return id;
  10. }
  11. public void setId(Long id) {
  12. this.id = id;
  13. }
  14. public String getUserName() {
  15. return userName;
  16. }
  17. public void setUserName(String userName) {
  18. this.userName = userName;
  19. }
  20. public void setPassword(String password) {
  21. this.password = password;
  22. }
  23. public String getRoles() {
  24. return roles;
  25. }
  26. public void setRoles(String roles) {
  27. this.roles = roles;
  28. }
  29. public Boolean getEnable() {
  30. return enable;
  31. }
  32. public void setEnable(Boolean enable) {
  33. this.enable = enable;
  34. }
  35. public List<GrantedAuthority> getAuthentications() {
  36. return authentications;
  37. }
  38. public void setAuthentications(List<GrantedAuthority> authentications) {
  39. this.authentications = authentications;
  40. }
  41. @Override
  42. public Collection<? extends GrantedAuthority> getAuthorities() {
  43. return this.getAuthentications();
  44. }
  45. @Override
  46. public String getPassword() {
  47. return this.password;
  48. }
  49. @Override
  50. public String getUsername() {
  51. return this.userName;
  52. }
  53. @Override
  54. public boolean isAccountNonExpired() {
  55. return true;
  56. }
  57. @Override
  58. public boolean isAccountNonLocked() {
  59. return true;
  60. }
  61. @Override
  62. public boolean isCredentialsNonExpired() {
  63. return true;
  64. }
  65. @Override
  66. public boolean isEnabled() {
  67. return this.enable;
  68. }
  69. }

实现UserDetails定义的几个方法:

  • isAccountNonExpiredisAccountNonLockedisCredentialsNonExpired 暂且用不到,统一返回rue,否则 Spring Security 会认为账号异常。
  • isEnabled对应enable字段,将其代入即可。
  • getAuthorities方法本身对应的是roles字段,但由于结构不一致,所以此处新建一个,并在后续进行填充。
  1. 数据库持久层

这里使用JPA 实现实体关系型映射,建立实体与数据库的关系:

(1)需要引入spring-boot-stater-data-jpa依赖

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-data-jpa</artifactId>
  4. </dependency>

(2)更改User为数据库映射实体类:

  1. @Entity
  2. @Table
  3. public class User implements UserDetails {
  4. @Id
  5. @GeneratedValue(strategy = GenerationType.SEQUENCE)
  6. private Long id;
  7. private String userName;
  8. private String password;
  9. private Boolean enable;
  10. private String roles;
  11. @Transient
  12. private List<GrantedAuthority> authentications;
  13. ....
  14. ....

(3)新建UserRepository

  1. public interface UserRepository extends JpaRepository<User,Long> {
  2. User findByUserName(String userName);
  3. }

自定义实现UserDetailsService

  1. @Service
  2. public class MyUserDetailServiceImpl implements UserDetailsService {
  3. @Autowired
  4. private UserRepository userRepository;
  5. @Override
  6. public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  7. //(1)从数据库获取用户
  8. User user = userRepository.findByUserName(username);
  9. if (user==null)//用户不存在
  10. throw new RuntimeException("用户"+username+"不存在!");
  11. //(2)将数据库中的roles解析为UserDetail的权限集
  12. String roles = user.getRoles();
  13. List<GrantedAuthority> grantedAuthorities = AuthorityUtils.commaSeparatedStringToAuthorityList(roles);
  14. user.setAuthentications(grantedAuthorities);
  15. return user;
  16. }
  17. }

AuthorityUtils.commaSeparatedStringToAuthorityList(String list) 是 spring security 提供的将逗号隔开的权限集字符串切割为权限对象列表,当然上面代码中我们也可以自己实现来代替:

  1. List<GrantedAuthority> getGrantedAuthorities(String roles){
  2. List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
  3. String[] split = StringUtils.split(roles, ";");
  4. for (int i = 0;i<split.length;i++){
  5. if (!StringUtils.isEmpty(split[i])){
  6. SimpleGrantedAuthority grantedAuthority = new SimpleGrantedAuthority(split[i]);
  7. grantedAuthorities.add(grantedAuthority);
  8. }
  9. }
  10. return grantedAuthorities;
  11. }

至此,我们就实现了 Spring Security 的自定义数据库结构认证工程。