Spring Security 由于配置復(fù)雜刃麸,一直被人所詬病,所以對(duì)于 SSM 框架的項(xiàng)目來(lái)說(shuō),輕量的 Shiro 顯然更適合它。然而 Spring Boot 的橫空出世打破了這個(gè)局面钩述,Spring Boot 通過(guò)自動(dòng)配置,使得開發(fā)者在 Spring Boot 中使用 Spring Security 變得非常簡(jiǎn)單∧滤椋現(xiàn)如今的 Spring Boot 應(yīng)用若是想集成安全框架牙勘,基本都會(huì)毫不猶豫地選擇 Spring Security。
HelloWorld 案例
首先通過(guò)一個(gè)案例來(lái)感受一下在 Spring Boot 中如何使用 Spring Security所禀,創(chuàng)建一個(gè) Spring Boot 應(yīng)用方面,并引入依賴:
<!--spring security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
編寫一個(gè)控制器:
@RestController
public class TestController {
@RequestMapping("hello")
public String hello(){
return "Hello SpringSecurity!";
}
}
然后直接啟動(dòng)項(xiàng)目,訪問(wèn) http://localhost:8080/login:
結(jié)果打開的是一個(gè)登錄頁(yè)面色徘,其實(shí)這時(shí)候我們的請(qǐng)求已經(jīng)被保護(hù)起來(lái)了葡幸,要想訪問(wèn),得先登錄贺氓。
在這個(gè)案例中僅僅是引入了一個(gè) Spring Security 的 starter 啟動(dòng)器蔚叨,沒(méi)有做任何的配置,而項(xiàng)目已經(jīng)具有了權(quán)限認(rèn)證辙培。
現(xiàn)在我們來(lái)登錄一下:
Spring Security 默認(rèn)提供了一個(gè)用戶名為 user 的用戶蔑水,其密碼在控制臺(tái)可以找到:
成功登錄以后就可以正常訪問(wèn)了:
用戶認(rèn)證
剛才的案例中我們使用的是 Spring Security 提供的用戶名和密碼進(jìn)行登錄的,那么該如何配置自己的用戶名和密碼呢扬蕊?
按照 Spring Boot 的自動(dòng)配置原理搀别,它肯定為其編寫了一個(gè) XXXProperties 的類作為配置,來(lái)查找一下:
找到了這個(gè)類就知道該如何配置了:
通過(guò)這幾個(gè)地方尾抑,我們能夠知道一些信息歇父,配置必須使用 spring.security 前綴,然后可以看到 Spring Security 為我們初始化的用戶名和密碼再愈,所以若是想修改配置榜苫,則應(yīng)使用 spring.security.user.name 和 spring.security.user.password。
在 Spring Boot 的配置文件中進(jìn)行如下配置:
spring:
security:
user:
name: wwj
password: 123
此時(shí)啟動(dòng)項(xiàng)目翎冲,將只能通過(guò)自己配置的用戶名和密碼登錄垂睬。
當(dāng)然還可以通過(guò)配置類的方式進(jìn)行配置,創(chuàng)建一個(gè)配置類繼承 WebSecurityConfigurerAdapter:
@Configuration
@Component
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//對(duì)密碼進(jìn)行加密
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String password = passwordEncoder.encode("123");
auth.inMemoryAuthentication().withUser("wwj").password(password).roles("admin");
}
}
重新啟動(dòng)項(xiàng)目測(cè)試一下。會(huì)發(fā)現(xiàn)登錄不上驹饺,觀察控制臺(tái):
這是因?yàn)槲覀冊(cè)趯?duì)密碼加密的時(shí)候使用到了 BCryptPasswordEncoder 對(duì)象钳枕,而容器中并沒(méi)有這個(gè)對(duì)象,所以我們還需要?jiǎng)?chuàng)建該對(duì)象:
@Bean
public PasswordEncoder getPasswordEncoder(){
return new BCryptPasswordEncoder();
}
再次重新啟動(dòng)一切正常赏壹。
我們還可以采取自定義實(shí)現(xiàn)類的方式來(lái)實(shí)現(xiàn)鱼炒,首先仍然是創(chuàng)建配置類:
@Configuration
@Component
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(getPasswordEncoder());
}
@Bean
public PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
此時(shí)我們需要實(shí)現(xiàn) UserDetailsService 接口:
@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
String password = new BCryptPasswordEncoder().encode("123");
//權(quán)限集合
List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
return new User("wwj", password, authorities);
}
}
查詢數(shù)據(jù)庫(kù)完成登錄認(rèn)證
剛才我們對(duì)案例進(jìn)行了進(jìn)一步的操作,即通過(guò)自己指定的用戶名和密碼進(jìn)行認(rèn)證蝌借,然而真實(shí)的生產(chǎn)環(huán)境中昔瞧,認(rèn)證的過(guò)程肯定是要經(jīng)過(guò)數(shù)據(jù)庫(kù)的,用戶輸入用戶名和密碼骨望,然后進(jìn)行數(shù)據(jù)庫(kù)查詢驗(yàn)證登錄硬爆,接下來(lái)就實(shí)現(xiàn)一下這個(gè)過(guò)程欣舵。
首先引入依賴:
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
創(chuàng)建數(shù)據(jù)表:
create database springsecurity;
use springsecurity;
create table user(
id int primary key auto_increment,
username varchar(20),
password varchar(20)
);
insert into user values(null,'zhangsan','123');
insert into user values(null,'lisi','456');
然后就可以使用 MyBatis 的逆向工程生成一下實(shí)體類擎鸠、Mapper 接口和 Mapper 配置文件,之后要在 Spring Boot 的配置文件中進(jìn)行 MyBatis 的相關(guān)配置:
spring:
datasource:
url: jdbc:mysql:///springsecurity?serverTimezone=GMT%2B8
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
mybatis:
type-aliases-package: com.wwj.springsecuritydemo.bean
mapper-locations: classpath:mappers/*.xml
最后在啟動(dòng)類上添加注解:
@SpringBootApplication
@MapperScan("com.wwj.springsecuritydemo.dao")
public class SpringsecuritydemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringsecuritydemoApplication.class, args);
}
}
這樣 MyBatis 就整合完成了缘圈,接下來(lái)是 Spring Security 的相關(guān)配置劣光,還記得我們是如何實(shí)現(xiàn)自定義用戶登錄的嗎?一起回憶一下吧糟把,首先需要一個(gè)配置類:
@Configuration
@Component
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(getPasswordEncoder());
}
@Bean
public PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
該配置類中注入了一個(gè) UserDetailsService 對(duì)象绢涡,它是一個(gè)接口,所以我們需要自定義類實(shí)現(xiàn)該接口:
@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
String password = new BCryptPasswordEncoder().encode("123");
//權(quán)限集合
List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
return new User("wwj", password, authorities);
}
}
之前我們是這樣寫的遣疯,直接返回 User 對(duì)象即可雄可,這個(gè) User 對(duì)象是 Spring Security 提供的,不是我們創(chuàng)建的實(shí)體類 User缠犀。
現(xiàn)在我們就需要修改這個(gè)類:
@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//這里的 String s 實(shí)際上是表單傳遞過(guò)來(lái)的用戶名
//根據(jù)用戶名查詢數(shù)據(jù)表
UserExample userExample = new UserExample();
UserExample.Criteria criteria = userExample.createCriteria();
criteria.andUsernameEqualTo(s);
List<com.wwj.springsecuritydemo.bean.User> userList = userMapper.selectByExample(userExample);
if (userList == null || userList.isEmpty()) {
//沒(méi)有查詢到用戶数苫,認(rèn)證失敗
throw new UsernameNotFoundException("該用戶不存在!");
}
//取出用戶信息
com.wwj.springsecuritydemo.bean.User user = userList.get(0);
String password = new BCryptPasswordEncoder().encode(user.getPassword());
//權(quán)限集合
List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
return new User(user.getUsername(), password, authorities);
}
}
首先 loadUserByUsername(String s) 方法的入?yún)?String s 實(shí)際上是表單傳遞過(guò)來(lái)的用戶名,然后通過(guò)該用戶名在數(shù)據(jù)表中查詢辨液,若查詢不到結(jié)果虐急,說(shuō)明用戶不存在,拋出異常即可滔迈;若查詢出了結(jié)果止吁,則需要將用戶信息封裝到 Spring Security 提供的 User 對(duì)象中進(jìn)行返回。
我們可以通過(guò) Debug 的方式來(lái)具體看看執(zhí)行流程燎悍,直接在第一行代碼上打個(gè)斷點(diǎn):
然后以 Debug 方式啟動(dòng):
當(dāng)輸入一個(gè)不存在的用戶并登錄時(shí):
可以看到此時(shí)的 s 就是我們輸入的用戶名敬惦,而當(dāng)我們輸入一個(gè)正確的用戶名時(shí):
loadUserByUsername() 方法同樣獲取到輸入的用戶名,
如果密碼輸入錯(cuò)誤也是無(wú)法進(jìn)行登錄的谈山,這是因?yàn)?Spring Security 有著它自己的驗(yàn)證方式仁热,因?yàn)槲覀兡壳斑€是用的 Spring Security 提供的登錄頁(yè)面,所以密碼的校驗(yàn)也是由 Spring Security 自己完成的。
自定義登錄頁(yè)面
剛才我們又對(duì)案例進(jìn)行了升級(jí)抗蠢,現(xiàn)在已經(jīng)可以根據(jù)數(shù)據(jù)表中的用戶信息進(jìn)行登錄校驗(yàn)了举哟,然而 Spring Security 提供的登錄頁(yè)面過(guò)于簡(jiǎn)單屎篱,那么該如何將其替換成我們自己的登錄頁(yè)面呢敞临?
首先來(lái)到配置類,在配置類中重寫 configure(HttpSecurity http) 方法:
@Configuration
@Component
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(getPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.and()
.authorizeRequests()
.antMatchers("/","/hello","/user/login")//配置哪些路徑可以直接訪問(wèn)
.permitAll()
.anyRequest().authenticated()//攔截所有資源
.and()
.formLogin()
.loginPage("/login.html")//設(shè)置登錄頁(yè)面
.loginProcessingUrl("/user/login")//設(shè)置登錄的請(qǐng)求路徑
.defaultSuccessUrl("/user/index")//設(shè)置登錄成功后的跳轉(zhuǎn)路徑
.permitAll()
.and()
.csrf().disable();//禁用 csrf
}
@Bean
public PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
此時(shí)就可以通過(guò) configure(HttpSecurity http) 方法的入?yún)?http 進(jìn)行相關(guān)的設(shè)置遭笋,需要注意的是秽褒,其中 loginProcessingUrl 方法設(shè)置的是登錄的請(qǐng)求路徑壶硅,即登錄表單的 action 屬性需要與其對(duì)應(yīng),登錄表單如下:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/user/login" method="post">
用戶名:<input type="text" name="username"/><br>
密碼:<input type="password" name="password"/><br>
<input type="submit" value="登錄">
</form>
</body>
</html>
這里注意了销斟,表單中的用戶名和密碼輸入框的 name 屬性值必須為 username 和 password庐椒,否則 Spring Security 就無(wú)法獲取到這兩個(gè)參數(shù),也就無(wú)法幫助你完成登錄校驗(yàn)了蚂踊。
最后編寫幾個(gè)控制方法進(jìn)行測(cè)試:
@RestController
public class TestController {
@RequestMapping("/hello")
public String hello(){
return "Hello SpringSecurity!";
}
@GetMapping("/user/index")
public String index(){
return "Hello Index!";
}
@GetMapping("/user/test")
public String test(){
return "Test!";
}
}
此時(shí)啟動(dòng)項(xiàng)目约谈,我們可以直接來(lái)訪問(wèn) http://localhost:8080/hello:
訪問(wèn)成功,這是因?yàn)槲覀兣渲昧?/hello 請(qǐng)求可以直接訪問(wèn)犁钟,那么接下來(lái)測(cè)試一下 http://localhost:8080/user/index:
可以看到棱诱,因?yàn)?/user/index 是被保護(hù)的,所以 Spring Security 幫助我們跳轉(zhuǎn)到了登錄頁(yè)面涝动,此時(shí)我們進(jìn)行登錄即可迈勋,登錄成功后就能正常訪問(wèn)了:
若是直接訪問(wèn)登錄頁(yè)面:
則登錄后會(huì)跳轉(zhuǎn)至 defaultSuccessUrl 方法配置的請(qǐng)求路徑中。
基于權(quán)限訪問(wèn)控制
前面我們已經(jīng)實(shí)現(xiàn)了資源的訪問(wèn)保護(hù)醋粟,然而并不是所有登錄認(rèn)證通過(guò)后的用戶都可以訪問(wèn)系統(tǒng)中的所有資源靡菇,我們應(yīng)該對(duì)用戶進(jìn)行權(quán)限的劃分,比如劃分為普通管理員和超級(jí)管理員權(quán)限米愿,那么普通管理員能夠操作的資源就肯定要少于超級(jí)管理員厦凤。
接下來(lái)就來(lái)看看在 Spring Security 中是如何實(shí)現(xiàn)權(quán)限訪問(wèn)控制的:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.and()
.authorizeRequests()
.antMatchers("/","/hello")
.permitAll()
//設(shè)置權(quán)限,當(dāng)前用戶必須有 admin 權(quán)限才能訪問(wèn)該路徑
.antMatchers("/user/index").hasAuthority("admin")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/user/login")
.defaultSuccessUrl("/user/index")
.permitAll()
.and()
.csrf().disable();
}
其實(shí)非常簡(jiǎn)單吗货,在原來(lái)的配置基礎(chǔ)上添加 hasAuthority 方法泳唠,該方法會(huì)判斷用戶是否擁有指定的權(quán)限,此時(shí)表示 /user/index 請(qǐng)求必須擁有 admin 權(quán)限才能夠訪問(wèn)宙搬,直接啟動(dòng)項(xiàng)目測(cè)試一下:
可以看到我們是能夠直接登錄成功的笨腥,這是因?yàn)樵?MyUserDetailsService 中我們?yōu)槊總€(gè)用戶都設(shè)置了 admin 權(quán)限:
下面我們修改一下權(quán)限:
List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("ord");
重新啟動(dòng)項(xiàng)目:
我們可以看到網(wǎng)頁(yè)報(bào)錯(cuò)了,顯示的是 403勇垛,即無(wú)權(quán)限訪問(wèn)脖母,當(dāng)然了,這個(gè)頁(yè)面我們也是可以進(jìn)行設(shè)置了闲孤。
有時(shí)候的一些資源是可以提供多個(gè)權(quán)限的用戶訪問(wèn)的谆级,這時(shí)我們就需要使用 hasAnyAuthoirty 方法為請(qǐng)求路徑設(shè)置多個(gè)權(quán)限:
//設(shè)置權(quán)限,當(dāng)前用戶必須有 admin 權(quán)限才能訪問(wèn)該路徑
.antMatchers("/user/test").hasAnyAuthority("admin,manager")
此時(shí) admin 和 manager 權(quán)限的用戶均可以訪問(wèn) /user/test 請(qǐng)求。
基于角色訪問(wèn)控制
角色與權(quán)限類似肥照,但又有些許不同脚仔,通常在一個(gè)系統(tǒng)中,權(quán)限不會(huì)直接分配給用戶舆绎,而是指定用戶為某個(gè)角色或某些角色鲤脏,并由這些角色來(lái)決定用戶具有哪些權(quán)限。比如在一個(gè)服裝后臺(tái)系統(tǒng)中吕朵,作為銷售角色的用戶猎醇,它就只有瀏覽衣服庫(kù)存和價(jià)錢的權(quán)限。
而在 Spring Security 中努溃,可以使用 hasRole 方法為某個(gè)請(qǐng)求設(shè)置角色訪問(wèn)控制:
//設(shè)置角色硫嘶,當(dāng)前用戶必須為 sale 角色才能訪問(wèn)該路徑
.antMatchers("/user/test").hasRole("sale")
此時(shí)表示 /user/test 請(qǐng)求只有 sale 角色的用戶才能訪問(wèn),在 MyUserDetailsService 中進(jìn)行設(shè)置:
List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_sale");
需要注意的是 hasRole 方法底層會(huì)為我們的角色名拼接一個(gè) ROLE_ 前綴梧税,所以在為用戶設(shè)置角色時(shí)需要加上該前綴:
private static String hasRole(String role) {
Assert.notNull(role, "role cannot be null");
if (role.startsWith("ROLE_")) {
throw new IllegalArgumentException("role should not start with 'ROLE_' since it is automatically inserted. Got '" + role + "'");
} else {
return "hasRole('ROLE_" + role + "')";
}
}
它同樣也可以設(shè)置多個(gè)角色沦疾,使用 hasAnyRole 方法即可,用法與 hasAnyAuthority 類似贡蓖。
自定義權(quán)限不足頁(yè)面
在前面我們實(shí)現(xiàn)了基于權(quán)限和角色的訪問(wèn)控制曹鸠,當(dāng)權(quán)限不足時(shí)煌茬,頁(yè)面會(huì)顯示 403斥铺,這種錯(cuò)誤對(duì)用戶來(lái)說(shuō)是不友好的,為此坛善,我們應(yīng)該自定義該頁(yè)面晾蜘,并讓其在權(quán)限不足,無(wú)法訪問(wèn)時(shí)跳轉(zhuǎn)至我們自己的頁(yè)面眠屎。
實(shí)現(xiàn)非常簡(jiǎn)單剔交,直接在 configure 方法中進(jìn)行配置即可:
//配置 403 頁(yè)面
http.exceptionHandling().accessDeniedPage("/403.html");
編寫一個(gè)頁(yè)面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1 style="color: red">您無(wú)權(quán)訪問(wèn)!</h1>
</body>
</html>
注解的使用
Spring Security 還支持注解的方式配置,下面介紹常用的五個(gè)注解:
- @Secured
- @PreAuthorize
- @PostAuthorize
- @PreFilter
- @PostFilter
@Secured 注解用于判斷用戶是否為某個(gè)角色改衩,注意這里也要加上 ROLE_ 前綴岖常,使用該注解前需要在啟動(dòng)類上添加一個(gè)注解:
@SpringBootApplication
@MapperScan("com.wwj.springsecuritydemo.dao")
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SpringsecuritydemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringsecuritydemoApplication.class, args);
}
}
此時(shí)在控制方法上添加該注解即可:
@GetMapping("/testSecured")
@Secured({"ROLE_sale","ROLE_manager"})
public String testSecured(){
return "testSecured";
}
這里表示只有用戶為 sale 或 manager 角色時(shí)才能訪問(wèn) /testSecured 請(qǐng)求。
@PreAuthorize 注解適合進(jìn)入方法前的權(quán)限驗(yàn)證葫督,使用該注解前需要在啟動(dòng)上添加 @EnableGlobalMethodSecurity(prePostEnabled = true) 注解:
@GetMapping("/testPreAuthorize")
@PreAuthorize("hasAnyAuthority('admin')")
public String testPreAuthorize() {
return "testSecured";
}
@PostAuthorize 注解會(huì)在方法執(zhí)行后再進(jìn)行權(quán)限驗(yàn)證竭鞍,適合帶有返回值的權(quán)限,它與 @PreAuthorize 的用法類似橄镜,加上不太常用偎快,這里就不做介紹了。
用戶注銷
登錄認(rèn)證之后洽胶,自然要有注銷功能晒夹,否則當(dāng)用戶準(zhǔn)備退出系統(tǒng)時(shí)會(huì)發(fā)現(xiàn)自己無(wú)法做到退出,導(dǎo)致一些安全問(wèn)題。
只需在配置類中添加如下配置即可:
//用戶注銷
http.logout().logoutUrl("/logout").logoutSuccessUrl("/login.html").permitAll();
此時(shí)已經(jīng)完成用戶注銷功能丐怯,但為了方便測(cè)試喷好,這里先創(chuàng)建一個(gè)登錄成功后跳轉(zhuǎn)的頁(yè)面 success.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>登錄成功!</h1>
<a href="/logout" style="color: red">注銷</a>
</body>
</html>
注銷超鏈接的 href 屬性需要填寫為在配置類中配置的 logoutUrl 屬性值。
然后在配置類中將登錄成功后的跳轉(zhuǎn) url 設(shè)置為該頁(yè)面:
.defaultSuccessUrl("/success.html")//設(shè)置登錄成功后的跳轉(zhuǎn)路徑
測(cè)試一下:
好了读跷,以上就是本篇文章的全部?jī)?nèi)容了绒窑,希望對(duì)你入門有幫助吧!如有錯(cuò)誤或未考慮完全的地方舔亭,望不吝賜教些膨!