環境準備
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
1.編寫實體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
字段,但由於結構不一致,所以此處新建一個,並在後續進行填充。
2.數據庫持久層
這裏使用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的自定義數據庫結構認證工程。