环境准备
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";}}
资源权限配置
@EnableWebSecuritypublic class MySecurityConfiguration extends WebSecurityConfigurerAdapter {@Overrideprotected 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即可:
@Beanpublic UserDetailsService userDetailsService(){UserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();//创建用户user01,密码user01,角色useruserDetailsManager.createUser(User.withUsername("user01").password("user01").roles("user").build());//创建用户admin01,密码admin01,角色adminuserDetailsManager.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:
@Beanpublic UserDetailsService userDetailsService(){InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();//创建用户user01,密码user01,角色useruserDetailsManager.createUser(User.withDefaultPasswordEncoder().username("user01").password("user01").roles("user").build());//创建用户admin01,密码admin01,角色adminuserDetailsManager.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.Driverspring.datasource.url=jdbc:postgresql://localhost:5432/springsecuritydemo?schema=publicspring.datasource.data-username=postgresspring.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 使用数据库来管理用户:
@Beanpublic UserDetailsService userDetailsService(DataSource dataSource){JdbcUserDetailsManager userDetailsManager = new JdbcUserDetailsManager();userDetailsManager.setDataSource(dataSource);//创建用户user01,密码user01,角色useruserDetailsManager.createUser(User.withDefaultPasswordEncoder().username("user01").password("user01").roles("user").build());//创建用户admin01,密码admin01,角色adminuserDetailsManager.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 字段中的内容)。
所以如果需要在服务启动时便生成部分用户,那么建议先判断用户名是否存在。
@Beanpublic UserDetailsService userDetailsService(DataSource dataSource){JdbcUserDetailsManager userDetailsManager = new JdbcUserDetailsManager();userDetailsManager.setDataSource(dataSource);//创建用户user01,密码user01,角色userif (!userDetailsManager.userExists("user01")) { //判断user01是否存在userDetailsManager.createUser(User.withDefaultPasswordEncoder().username("user01").password("user01").roles("user").build());}//创建用户admin01,密码admin01,角色adminif (!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同样允许我们配置认证用户:
@EnableWebSecuritypublic class MySecurityConfiguration extends WebSecurityConfigurerAdapter {@Overrideprotected 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;// }@Overrideprotected 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;}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return this.getAuthentications();}@Overridepublic String getPassword() {return this.password;}@Overridepublic String getUsername() {return this.userName;}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic 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@Tablepublic class User implements UserDetails {@Id@GeneratedValue(strategy = GenerationType.SEQUENCE)private Long id;private String userName;private String password;private Boolean enable;private String roles;@Transientprivate List<GrantedAuthority> authentications;........
(3)新建UserRepository
public interface UserRepository extends JpaRepository<User,Long> {User findByUserName(String userName);}
自定义实现UserDetailsService
@Servicepublic class MyUserDetailServiceImpl implements UserDetailsService {@Autowiredprivate UserRepository userRepository;@Overridepublic 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 的自定义数据库结构认证工程。
