盡管項目啟動時,Spring Security會提供了默認的用戶信息,可以快速認證和啟動,但大多數應用程序都希望使用自定義的用戶認證。對于自定義用戶認證,Spring Security提供了多種認證方式,常用的有In-Memory Authentication(內存身份認證)、JDBC Authentication(JDBC身份認證)和UserDetailsService(身份詳情服務)。下面對Spring Security的這三種自定義身份認證進行詳細講解。
1.內存身份認證
以內存身份認證時,需要在Spring Security的相關組件中進行指定當前認證方式為內存身份認證。Spring Security 5.7.1開始Spring Security將WebSecurityConfigurerAdapter類標注為過時,推薦直接聲明配置類,在配置類中直接定義組件的信息。 本書使用Spring Boot 2.7.6,其對應的Spring Security版本為5.7.5。自定義內存身份認證時,可以通過InMemoryUserDetailsManager類實現,InMemoryUserDetailsManager是UserDetailsService的一個實現類,方便在內存中創建一個用戶。對此,只需 在自定義配置類中創建InMemoryUserDetailsManager實例,在該實例中指定該實例的認證信息,并存入在Spring容器中即可。
(1)創建配置類
創建一個配置類WebSecurityConfig,在該類中創建UserDetailsService類型的InMemoryUserDetailsManager實例對象交由Spring容器管理
@Configuration
public class WebSecurityConfig {
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager users = new InMemoryUserDetailsManager();
users.createUser(User.withUsername("zhangsan")
.password("{noop}1234")
.roles("ADMIN")
.build());
return users;
}
}
進行自定義用戶認證時,需要注意以下幾個問題。
提交認證時會對輸入的密碼使用密碼編譯器進行加密并與正確的密碼進行校驗。如果不想要對輸入的密碼進行加密,需要在密碼前對使用{noop}進行標注。
從Spring Security 5開始,自定義用戶認證如果沒有設置密碼編碼器,也沒有在密碼前使用{noop}進行標注,會認證失敗。
自定義用戶認證時,可以定義用戶角色roles,也可以定義用戶權限authorities,在進行賦值時,權限通常是在角色值的基礎上添加“ROLE_”前綴。
自定義用戶認證時,可以為某個用戶一次指定多個角色或權限。
(2)驗證內存身份認證
啟動項目后,查看控制臺輸出的信息,發現沒有默認安全管理時隨機生成了密碼。
在瀏覽器訪問項目首頁“http://localhost:8080/”。
2.JDBC身份認證
JDBC身份認證是通過JDBC連接數據庫,基于數據庫中已有的用戶信息進行身份認證,這樣避免了內存身份認證的弊端,可以實現對系統已注冊的用戶進行身份認證。JdbcUserDetailsManager是Spring Security內置的UserDetailsService的實現類,使用JdbcUserDetailsManager可以通過JDBC將數據庫和Spring Security連接起來。下面對JDBC身份認證方式進行講解。
(1)數據準備
使用之前創建的名為springbootdata的數據庫,在該數據庫中創建三個表user、priv和user_priv,并預先插入幾條測試數據。準備數據的SQL語句如下。?
#選擇使用數據庫
USEspringbootdata;
#創建表user并插入相關數據
DROPTABLEIFEXISTS`user`;
CREATETABLE`user`(
`id`int(20)NOTNULLAUTO_INCREMENT,
`username`varchar(200)DEFAULTNULL,
`password`varchar(200)DEFAULTNULL,
`valid`tinyint(1)NOTNULLDEFAULT1,
PRIMARYKEY(`id`)
)ENGINE=InnoDBAUTO_INCREMENT=4DEFAULTCHARSET=utf8;
INSERTINTO`user`VALUES('1','zhangsan',
'$2a$10$7fWqX7Y010pMnyym/AHZX.3chIbnPZbj3N/iqcG4APCF.hC6CMh5a','1');
INSERTINTO`user`VALUES('2','lisi',
'$2a$10$7fWqX7Y010pMnyym/AHZX.3chIbnPZbj3N/iqcG4APCF.hC6CMh5a','1');
#創建表priv并插入相關數據
DROPTABLEIFEXISTS`priv`;
CREATETABLE`priv`(
`id`int(20)NOTNULLAUTO_INCREMENT,
`authority`varchar(20)DEFAULTNULL,
PRIMARYKEY(`id`)
)ENGINE=InnoDBAUTO_INCREMENT=3DEFAULTCHARSET=utf8;
INSERTINTO`priv`VALUES('1','ROLE_COMMON');
INSERTINTO`priv`VALUES('2','ROLE_ADMIN');
#創建表user_priv并插入相關數據
DROPTABLEIFEXISTS`user_priv`;
CREATETABLE`user_priv`(
`id`int(20)NOTNULLAUTO_INCREMENT,
`user_id`int(20)DEFAULTNULL,
`priv_id`int(20)DEFAULTNULL,
PRIMARYKEY(`id`)
)ENGINE=InnoDBAUTO_INCREMENT=5DEFAULTCHARSET=utf8;
INSERTINTO`user_priv`VALUES('1','1','1');
INSERTINTO`user_priv`VALUES('2','2','2');
?(2)配置依賴
添加JDBC的啟動器依賴。
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
(3)設置配置信息
設置數據庫連接的相關配置信息
(4)修改配置類
修改WebSecurityConfig配置類userDetailsService()方法,將該方法創建的實例對象修改為JdbcUserDetailsManager
@Configuration
public class WebSecurityConfig {
@Autowired
private DataSource dataSource;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService() {
String userSQL ="SELECT username,password, valid " +
"FROM user WHERE username = ?";
String authoritySQL="SELECT u.username,p.authority " +
"FROM user u,priv p,user_priv up " +
"WHERE up.user_id=u.id AND up.priv_id=p.id and u.username =?";
JdbcUserDetailsManager users = new JdbcUserDetailsManager();
users.setDataSource(dataSource);
users.setUsersByUsernameQuery(userSQL);
users.setAuthoritiesByUsernameQuery(authoritySQL);
return users;
}
}
(5)效果測試
重啟項目進行效果測試
3.自定義UserDetailsService身份認證
使用InMemoryUserDetailsManager和JdbcUserDetailsManager進行身份認證時,其真正的認證邏輯都在UserDetailsService接口重寫的loadUserByUsername()方法中。對于一個完善的項目來說,通常會實現用戶信息查詢服務,對此可以自定義一個UserDetailsService實現類,重寫該接口的loadUserByUsername()方法,在該方法中查詢用戶信息,將查詢到的用戶信息填充到UserDetails對象返回,以實現用戶的身份認證。下面通過案例對自定義UserDetailsService進行身份驗證的實現進行演示 。
(1)創建實體類
在子包entity下創建用戶實體類UserDto和權限實體類Privilege
public class UserDto {
private Integer id; //用戶編號
private String username; //用戶名稱
private String password; //密碼
private Integer valid; //是否合法public Integer getId() {
return id;
}public void setId(Integer id) {
this .id = id;
}public String getUsername() {
return username;
}public void setUsername(String username) {
this .username = username;
}public String getPassword() {
return password;
}public void setPassword(String password) {
this .password = password;
}public Integer getValid() {
return valid;
}public void setValid(Integer valid) {
this .valid = valid;
}
}
public class Privilege {
private Integer id; //編號
private String authority; //權限public Integer getId() {
return id;
}public void setId(Integer id) {
this .id = id;
}public String getAuthority() {
return authority;
}public void setAuthority(String authority) {
this .authority = authority;
}
}
?(2)創建用戶持久層接口
在dao子包下創建用戶持久層接口,在接口中定義查詢用戶及角色信息的方法
@Repository
public class UserDao {
@Autowired
JdbcTemplate jdbcTemplate;
//根據賬號查詢用戶信息
public UserDto getUserByUsername(String username){
String sql = "SELECT * FROM user WHERE username = ?";
//連接數據庫查詢用戶
List<UserDto> list = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(UserDto.class ),username);
if (list !=null && list.size()==1){
return list.get(0);
}
return null;
}
//根據用戶id查詢用戶權限
public List<String> findPrivilegesByUserId(Integer userId){
String sql = "SELECT u.username,p.authority " +
"FROM user u,priv p,user_priv up " +
"WHERE up.user_id=u.id AND up.priv_id=p.id and u.id =?";
List<Privilege> list = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Privilege.class ), userId);
List<String> privileges = new ArrayList<>();
list.forEach(p -> privileges.add(p.getAuthority()));
return privileges;
}
}
(3)封裝用戶認證信息
在service子包下創建UserDetailsServiceImpl類,該類實現UserDetailsService接口,并在重寫的loadUserByUsername()方法中封裝用戶認證信息
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
UserDao userDao;
//根據用戶名查詢用戶信息
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//連接數據庫根據賬號查詢用戶信息
UserDto userDto = userDao.getUserByUsername(username);
if (userDto == null){
//如果用戶查不到,返回null,會拋出異常
return null;
}
//根據用戶的id查詢用戶的權限
List<String> privileges = userDao.findPrivilegesByUserId(userDto.getId());
//將privileges轉成數組
String[] privilegeArray = new String[privileges.size()];
privileges.toArray(privilegeArray);
UserDetails userDetails = User.withUsername(userDto.getUsername()).password(userDto.getPassword()).authorities(privilegeArray).build();
return userDetails;
}
}
(4)效果測試
將userDetailsService()方法進行注釋,使用自定義UserDetailsService身份認證。
重啟項目進行效果測試。