手把手帶你入門 Spring Security

Spring Security 由于配置復(fù)雜,一直被人所詬病,所以對于 SSM 框架的項目來說,輕量的 Shiro 顯然更適合它。然而 Spring Boot 的橫空出世打破了這個局面,Spring Boot 通過自動配置,使得開發(fā)者在 Spring Boot 中使用 Spring Security 變得非常簡單?,F(xiàn)如今的 Spring Boot 應(yīng)用若是想集成安全框架,基本都會毫不猶豫地選擇 Spring Security。

HelloWorld 案例

首先通過一個案例來感受一下在 Spring Boot 中如何使用 Spring Security,創(chuàng)建一個 Spring Boot 應(yīng)用,并引入依賴:

<!--spring security-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

編寫一個控制器:

@RestController
public class TestController {

    @RequestMapping("hello")
    public String hello(){
        return "Hello SpringSecurity!";
    }
}

然后直接啟動項目,訪問 http://localhost:8080/login

結(jié)果打開的是一個登錄頁面,其實這時候我們的請求已經(jīng)被保護起來了,要想訪問,得先登錄。

在這個案例中僅僅是引入了一個 Spring Security 的 starter 啟動器,沒有做任何的配置,而項目已經(jīng)具有了權(quán)限認證。

現(xiàn)在我們來登錄一下:

手把手帶你入門 Spring Security

Spring Security 默認提供了一個用戶名為 user 的用戶,其密碼在控制臺可以找到:

成功登錄以后就可以正常訪問了:

手把手帶你入門 Spring Security

用戶認證

剛才的案例中我們使用的是 Spring Security 提供的用戶名和密碼進行登錄的,那么該如何配置自己的用戶名和密碼呢?

按照 Spring Boot 的自動配置原理,它肯定為其編寫了一個 XXXProperties 的類作為配置,來查找一下:

手把手帶你入門 Spring Security

找到了這個類就知道該如何配置了:

通過這幾個地方,我們能夠知道一些信息,配置必須使用 spring.security 前綴,然后可以看到 Spring Security 為我們初始化的用戶名和密碼,所以若是想修改配置,則應(yīng)使用 spring.security.user.name 和 spring.security.user.password。

在 Spring Boot 的配置文件中進行如下配置:

spring:
  security:
    user:
      name: wwj
      password: 123

此時啟動項目,將只能通過自己配置的用戶名和密碼登錄。

當然還可以通過配置類的方式進行配置,創(chuàng)建一個配置類繼承 WebSecurityConfigurerAdapter:

@Configuration
@Component
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //對密碼進行加密
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String password = passwordEncoder.encode("123");
        auth.inMemoryAuthentication().withUser("wwj").password(password).roles("admin");
    }
}

重新啟動項目測試一下。會發(fā)現(xiàn)登錄不上,觀察控制臺:

這是因為我們在對密碼加密的時候使用到了 BCryptPasswordEncoder 對象,而容器中并沒有這個對象,所以我們還需要創(chuàng)建該對象:

@Bean
public PasswordEncoder getPasswordEncoder(){
    return new BCryptPasswordEncoder();
}

再次重新啟動一切正常。

我們還可以采取自定義實現(xiàn)類的方式來實現(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();
    }
}

此時我們需要實現(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ù)庫完成登錄認證

剛才我們對案例進行了進一步的操作,即通過自己指定的用戶名和密碼進行認證,然而真實的生產(chǎn)環(huán)境中,認證的過程肯定是要經(jīng)過數(shù)據(jù)庫的,用戶輸入用戶名和密碼,然后進行數(shù)據(jù)庫查詢驗證登錄,接下來就實現(xiàn)一下這個過程。

首先引入依賴:

<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 的逆向工程生成一下實體類、Mapper 接口和 Mapper 配置文件,之后要在 Spring Boot 的配置文件中進行 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

最后在啟動類上添加注解:

@SpringBootApplication
@MapperScan("com.wwj.springsecuritydemo.dao")
public class SpringsecuritydemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringsecuritydemoApplication.class, args);
    }

}

這樣 MyBatis 就整合完成了,接下來是 Spring Security 的相關(guān)配置,還記得我們是如何實現(xiàn)自定義用戶登錄的嗎?一起回憶一下吧,首先需要一個配置類:

@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();
    }
}

該配置類中注入了一個 UserDetailsService 對象,它是一個接口,所以我們需要自定義類實現(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 對象即可,這個 User 對象是 Spring Security 提供的,不是我們創(chuàng)建的實體類 User。

現(xiàn)在我們就需要修改這個類:

@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        //這里的 String s 實際上是表單傳遞過來的用戶名
        //根據(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()) {
            //沒有查詢到用戶,認證失敗
            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ù)據(jù)表中查詢,若查詢不到結(jié)果,說明用戶不存在,拋出異常即可;若查詢出了結(jié)果,則需要將用戶信息封裝到 Spring Security 提供的 User 對象中進行返回。

我們可以通過 Debug 的方式來具體看看執(zhí)行流程,直接在第一行代碼上打個斷點:

手把手帶你入門 Spring Security

然后以 Debug 方式啟動:

手把手帶你入門 Spring Security

當輸入一個不存在的用戶并登錄時:

手把手帶你入門 Spring Security

可以看到此時的 s 就是我們輸入的用戶名,而當我們輸入一個正確的用戶名時:

手把手帶你入門 Spring Security

loadUserByUsername() 方法同樣獲取到輸入的用戶名,

手把手帶你入門 Spring Security

如果密碼輸入錯誤也是無法進行登錄的,這是因為 Spring Security 有著它自己的驗證方式,因為我們目前還是用的 Spring Security 提供的登錄頁面,所以密碼的校驗也是由 Spring Security 自己完成的。

自定義登錄頁面

剛才我們又對案例進行了升級,現(xiàn)在已經(jīng)可以根據(jù)數(shù)據(jù)表中的用戶信息進行登錄校驗了,然而 Spring Security 提供的登錄頁面過于簡單,那么該如何將其替換成我們自己的登錄頁面呢?

首先來到配置類,在配置類中重寫 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")//配置哪些路徑可以直接訪問
                .permitAll()
                .anyRequest().authenticated()//攔截所有資源
                .and()
                .formLogin()
                .loginPage("/login.html")//設(shè)置登錄頁面
                .loginProcessingUrl("/user/login")//設(shè)置登錄的請求路徑
                .defaultSuccessUrl("/user/index")//設(shè)置登錄成功后的跳轉(zhuǎn)路徑
                .permitAll()
                .and()
                .csrf().disable();//禁用 csrf
    }

    @Bean
    public PasswordEncoder getPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

此時就可以通過 configure(HttpSecurity http) 方法的入?yún)?http 進行相關(guān)的設(shè)置,需要注意的是,其中 loginProcessingUrl 方法設(shè)置的是登錄的請求路徑,即登錄表單的 action 屬性需要與其對應(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 就無法獲取到這兩個參數(shù),也就無法幫助你完成登錄校驗了。

最后編寫幾個控制方法進行測試:

@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!";
    }
}

此時啟動項目,我們可以直接來訪問 http://localhost:8080/hello

訪問成功,這是因為我們配置了 /hello 請求可以直接訪問,那么接下來測試一下 http://localhost:8080/user/index

手把手帶你入門 Spring Security

可以看到,因為 /user/index 是被保護的,所以 Spring Security 幫助我們跳轉(zhuǎn)到了登錄頁面,此時我們進行登錄即可,登錄成功后就能正常訪問了:

手把手帶你入門 Spring Security

若是直接訪問登錄頁面:

手把手帶你入門 Spring Security

則登錄后會跳轉(zhuǎn)至 defaultSuccessUrl 方法配置的請求路徑中。

基于權(quán)限訪問控制

前面我們已經(jīng)實現(xiàn)了資源的訪問保護,然而并不是所有登錄認證通過后的用戶都可以訪問系統(tǒng)中的所有資源,我們應(yīng)該對用戶進行權(quán)限的劃分,比如劃分為普通管理員和超級管理員權(quán)限,那么普通管理員能夠操作的資源就肯定要少于超級管理員。

接下來就來看看在 Spring Security 中是如何實現(xiàn)權(quán)限訪問控制的:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .and()
        .authorizeRequests()
        .antMatchers("/","/hello")
        .permitAll()
        //設(shè)置權(quán)限,當前用戶必須有 admin 權(quán)限才能訪問該路徑
        .antMatchers("/user/index").hasAuthority("admin")
        .anyRequest().authenticated()
        .and()
        .formLogin()
        .loginPage("/login.html")
        .loginProcessingUrl("/user/login")
        .defaultSuccessUrl("/user/index")
        .permitAll()
        .and()
        .csrf().disable();
}

其實非常簡單,在原來的配置基礎(chǔ)上添加 hasAuthority 方法,該方法會判斷用戶是否擁有指定的權(quán)限,此時表示 /user/index 請求必須擁有 admin 權(quán)限才能夠訪問,直接啟動項目測試一下:

手把手帶你入門 Spring Security

可以看到我們是能夠直接登錄成功的,這是因為在 MyUserDetailsService 中我們?yōu)槊總€用戶都設(shè)置了 admin 權(quán)限:

手把手帶你入門 Spring Security

下面我們修改一下權(quán)限:

List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("ord");

重新啟動項目:

手把手帶你入門 Spring Security

我們可以看到網(wǎng)頁報錯了,顯示的是 403,即無權(quán)限訪問,當然了,這個頁面我們也是可以進行設(shè)置了。

有時候的一些資源是可以提供多個權(quán)限的用戶訪問的,這時我們就需要使用 hasAnyAuthoirty 方法為請求路徑設(shè)置多個權(quán)限:

//設(shè)置權(quán)限,當前用戶必須有 admin 權(quán)限才能訪問該路徑
.antMatchers("/user/test").hasAnyAuthority("admin,manager")

此時 admin 和 manager 權(quán)限的用戶均可以訪問 /user/test 請求。

基于角色訪問控制

角色與權(quán)限類似,但又有些許不同,通常在一個系統(tǒng)中,權(quán)限不會直接分配給用戶,而是指定用戶為某個角色或某些角色,并由這些角色來決定用戶具有哪些權(quán)限。比如在一個服裝后臺系統(tǒng)中,作為銷售角色的用戶,它就只有瀏覽衣服庫存和價錢的權(quán)限。

而在 Spring Security 中,可以使用 hasRole 方法為某個請求設(shè)置角色訪問控制:

//設(shè)置角色,當前用戶必須為 sale 角色才能訪問該路徑
.antMatchers("/user/test").hasRole("sale")

此時表示 /user/test 請求只有 sale 角色的用戶才能訪問,在 MyUserDetailsService 中進行設(shè)置:

List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_sale");

需要注意的是 hasRole 方法底層會為我們的角色名拼接一個 ROLE_ 前綴,所以在為用戶設(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è)置多個角色,使用 hasAnyRole 方法即可,用法與 hasAnyAuthority 類似。

自定義權(quán)限不足頁面

在前面我們實現(xiàn)了基于權(quán)限和角色的訪問控制,當權(quán)限不足時,頁面會顯示 403,這種錯誤對用戶來說是不友好的,為此,我們應(yīng)該自定義該頁面,并讓其在權(quán)限不足,無法訪問時跳轉(zhuǎn)至我們自己的頁面。

實現(xiàn)非常簡單,直接在 configure 方法中進行配置即可:

//配置 403 頁面
http.exceptionHandling().accessDeniedPage("/403.html");

編寫一個頁面:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1 style="color: red">您無權(quán)訪問!</h1>
</body>
</html>

手把手帶你入門 Spring Security

注解的使用

Spring Security 還支持注解的方式配置,下面介紹常用的五個注解:

  • @Secured
  • @PreAuthorize
  • @PostAuthorize
  • @PreFilter
  • @PostFilter

@Secured 注解用于判斷用戶是否為某個角色,注意這里也要加上 ROLE_ 前綴,使用該注解前需要在啟動類上添加一個注解:

@SpringBootApplication
@MapperScan("com.wwj.springsecuritydemo.dao")
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SpringsecuritydemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringsecuritydemoApplication.class, args);
    }
}

此時在控制方法上添加該注解即可:

@GetMapping("/testSecured")
@Secured({"ROLE_sale","ROLE_manager"})
public String testSecured(){
    return "testSecured";
}

這里表示只有用戶為 sale 或 manager 角色時才能訪問 /testSecured 請求。

@PreAuthorize 注解適合進入方法前的權(quán)限驗證,使用該注解前需要在啟動上添加 @EnableGlobalMethodSecurity(prePostEnabled = true) 注解:

@GetMapping("/testPreAuthorize")
@PreAuthorize("hasAnyAuthority('admin')")
public String testPreAuthorize() {
    return "testSecured";
}

@PostAuthorize 注解會在方法執(zhí)行后再進行權(quán)限驗證,適合帶有返回值的權(quán)限,它與 @PreAuthorize 的用法類似,加上不太常用,這里就不做介紹了。

用戶注銷

登錄認證之后,自然要有注銷功能,否則當用戶準備退出系統(tǒng)時會發(fā)現(xiàn)自己無法做到退出,導致一些安全問題。

只需在配置類中添加如下配置即可:

//用戶注銷
http.logout().logoutUrl("/logout").logoutSuccessUrl("/login.html").permitAll();

此時已經(jīng)完成用戶注銷功能,但為了方便測試,這里先創(chuàng)建一個登錄成功后跳轉(zhuǎn)的頁面 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è)置為該頁面:

.defaultSuccessUrl("/success.html")//設(shè)置登錄成功后的跳轉(zhuǎn)路徑

測試一下:

手把手帶你入門 Spring Security

好了,以上就是本篇文章的全部內(nèi)容了,希望對你入門有幫助吧!如有錯誤或未考慮完全的地方,望不吝賜教!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容