SpringBoot整合SpringSecurity

先說一下SpringSecurity是干什么的,SpringSecurity主要作用有2方面:認證、授權(quán)。

  • 認證:Authentication, 用戶認證就是判斷一個用戶的身份是否合法的過程,用戶去訪問系統(tǒng)資源時系統(tǒng)要求驗證用戶的身份信息,身份合法方可繼續(xù)訪問,不合法則拒絕訪問。常見的用戶身份認證方式有:用戶名密碼登錄,二維碼登錄,手機短信登錄,指紋認證等方式。
  • 授權(quán): Authorize,授權(quán)是用戶認證通過根據(jù)用戶的權(quán)限來控制用戶訪問資源的過程,擁有資源的訪問權(quán)限則正常訪問,沒有權(quán)限則拒絕訪問

權(quán)限管理涉及到幾個概念:

  • 主體(用戶id、賬號、密碼、...)
  • 資源(資源id、資源名稱、訪問地址、...)
  • 權(quán)限(權(quán)限id、權(quán)限標識、權(quán)限名稱、資源id、...)
  • 角色(角色id、角色名稱、...)

業(yè)界通?;赗BAC實現(xiàn)授權(quán)。
在單體應(yīng)用中,我覺得理解為基于角色的訪問控制(Role-Based Access Control)是比較合適的,用起來比較方便。
而在當前動輒微服務(wù)開發(fā)的環(huán)境下,個人覺得理解為基于資源的訪問控制(Resource-Based Access Control)用起來更方便,因為微服務(wù)中各個微服務(wù)都當做資源來看待了。
整合
SpringBoot整合SpringSecurity還是比較簡單的:

  1. 引入相關(guān)jar包
  2. 配置Security(配置時會稍麻煩,因為需要理解的比較多)

1. 引入Jar包

比較簡單,引入web包和security包就行

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

2. 啟動測試

引入jar包后就可以啟動了,啟動時會生成隨機密碼:


隨機密碼
  • 訪問項目就會跳轉(zhuǎn)到登陸頁面,默認賬號:user

    登陸

  • SpringSecurity自帶注銷地址:/logout,訪問這個地址會彈出注銷頁面。

    注銷

3. 自定義配置

以上是SpringSecurity自帶的認證功能,我們使用時需要根據(jù)我們自己的需要自定義一些內(nèi)容(2方面配置:認證配置,授權(quán)配置),例如:

  • 登陸的賬號密碼
  • 是否允許表單登陸
  • 密碼加密的情況
  • 權(quán)限鑒定
  • ......

3.1 認證配置

自定義的配置其實都在同一個類中,認證和授權(quán)在不同的方法中,配置類繼承WebSecurityConfigurerAdapter類,重寫2個方法就行。
注意,就是這個父類,想要配置什么,點進源碼去里面找對應(yīng)的方法
認證的方式有2種:一是賬號密碼等認證信息寫在配置中,二是賬號密碼等信息從數(shù)據(jù)庫讀取。使用時,取其一

3.1.1 認證信息寫在配置中
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 1. 認證配置
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 測試用的,寫死的賬號密碼
        auth.inMemoryAuthentication()
            .withUser("admin")
            .password(passwordEncoder().encode("123456"))
            .roles("ADMIN")
            .authorities("/test/t1")
            .and()
            .withUser("user")
            .password(passwordEncoder().encode("123456"))
            .roles("USER")
            .authorities("/test/t2")
            ;
    }
    
   // 設(shè)置密碼加密方法
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 2. 授權(quán)的配置方法,下面再講,先空著
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception { 
        
    }   
}
3.1.2 認證信息從數(shù)據(jù)庫拿

這種方式,配置類簡單,但是需要一個用戶服務(wù)類,來返回一個SpringSecurity封裝的一個user對象,直接將服務(wù)類放到配置文件中就行。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    // 注入服務(wù)類
    @Autowired 
    private UserDetailsServiceImpl userDetailsServiceImpl;
    
    /**
     * 1. 認證配置
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsServiceImpl);
    }
    
    // 設(shè)置密碼加密方法,必須設(shè)置
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 2. 授權(quán)的配置方法,下面再講,先空著
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception { 
        
    }
}

看一下這個服務(wù)類怎么寫的,先準備一個服務(wù)類要返回的UserDetails對象

  • 自定義user實體對象
package com.example.demo.security.userdetails;

import java.util.Collection;
import java.util.Date;

import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import com.fasterxml.jackson.annotation.JsonFormat;

import lombok.Data;

/**
 * 參考{@link org.springframework.security.core.userdetails.User}這個類,
 * 這個類是security設(shè)置實體類參數(shù)值的時候用的,里面很多方法可以參考使用。
 * 比如設(shè)置roles和設(shè)置authorities的過程,在User類的內(nèi)部類UserBuilder中
 */

@Data
public class MyUserDetail implements UserDetails {

    private static final long serialVersionUID = 1L;

    
    private Long userId;

    private String username;

    private String name;

    private String password;
    
    private boolean status;

    private Long deptId;

    private String email;

    private String mobile;

    private String sex;

    private String avatar;

    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8")
    private Date lastLoginTime;
    
    // 角色權(quán)限:SpringSecurity中角色和權(quán)限都是放在這個里面的,使用起來是一樣的,區(qū)別在于,角色要加前綴 ROLE_
    private Collection<GrantedAuthority> authorities;
    
    /**
     * 參考{@link org.springframework.security.core.userdetails.User.UserBuilder}
     * 中的roles方法
     * @param roles
     * @return
     */
    public List<GrantedAuthority> roles(String... roles) {
        List<GrantedAuthority> authorities = new ArrayList<>(roles.length);
        for (String role : roles) {
            Assert.isTrue(!role.startsWith("ROLE_"),
                    () -> role + " cannot start with ROLE_ (it is automatically added)");
            authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
        }
        return authorities;
    }
    
    /**
     * 參考{@link org.springframework.security.core.userdetails.User.UserBuilder}
     * 中的 authorities 方法
     * @param authorities
     * @return
     */
    public List<GrantedAuthority> authorities(String authorities) {
        return AuthorityUtils.commaSeparatedStringToAuthorityList(authorities);
    }
    
    public List<GrantedAuthority> authorities(String... authorities) {
        return AuthorityUtils.createAuthorityList(authorities);
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }

    @Override
    public boolean isAccountNonExpired() {
        // 先按這個判斷,需要什么自己添加
        return this.isStatus();
    }

    @Override
    public boolean isAccountNonLocked() {
        return this.isStatus();
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return this.isStatus();
    }

    @Override
    public boolean isEnabled() {
        return this.isStatus();
    }
}
  • 用戶服務(wù)類
package com.example.demo.security.userdetails;

import java.util.ArrayList;
import java.util.List;
import javax.annotation.Resource;

import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

import com.example.demo.dao.MyUserDao;

public class UserDetailsServiceImpl implements UserDetailsService {

    @Resource
    private MyUserDao myUserDao;
    
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        
        MyUserDetail userDetail = myUserDao.getMyUserDetail(username);
        // 注意數(shù)據(jù)庫中保存的密碼,要是加密過的,就是在配置類中設(shè)置的加密方法
        if (!userDetail.isEnabled()) {
            throw new DisabledException("賬號狀態(tài)異常!");
        } else if (!userDetail.isCredentialsNonExpired()) {
            throw new LockedException("密碼過期!");
        }
        
        // 模擬一點角色權(quán)限信息,角色前面要加 ROLE_ 前綴
        userDetail.setAuthorities(userDetail.authorities("/test/t1", "/test/t2", "ROLE_ADMIN", "ROLE_ROOT"));
        
        return userDetail;
    }

}

使用上面這2種方法之一,我們就可以用我們自己的賬號和密碼登陸了。

3.1.3 看看SpringSecurity自帶的

上面是我們自己實現(xiàn)的接口,寫了過程。其實,SpringSecurity自己也封裝了很多,我們也可以看看。

實現(xiàn)類

官方包里面的就是用戶的實現(xiàn)過程,其實我們可以用自帶的這些,但是限制比較多,拿jdbc這個來說,他也重寫了loadUsersByUsername()。
JdbcUserDetailsManager

但是它限制了很多東西,表名、字段等要符合人家要求:你要有users表,表中要包含這些字段:
sql

如果你要想使用,初始化時傳入DataSource即可,他會根據(jù)你傳入的數(shù)據(jù)源自動查找數(shù)據(jù)。
構(gòu)造方法

3.2 授權(quán)配置

注意:前提條件是,認證時,賬號信息中加入了角色和權(quán)限的一些信息,這里才能進行權(quán)限判定。
授權(quán)配置常用的有2種方式,一是在SecurityConfig類中,一是用注解表達式。

3.2.1 在SecurityConfig類配置權(quán)限

授權(quán)配置還是在上面的SecurityConfig類中,只不過是在下面的那個方法中配置。

package com.example.demo.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import com.example.demo.entity.Result;
import com.example.demo.security.userdetails.UserDetailsServiceImpl;

import cn.hutool.json.JSONUtil;

/**
 * SpringSecurity配置
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    // 注入服務(wù)類
    @Autowired 
    private UserDetailsServiceImpl userDetailsServiceImpl;
    
    /**
     * 1. 認證配置
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsServiceImpl);
    }
    
    // 設(shè)置密碼加密方法
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    
    /**
     * 2. 授權(quán)的配置方法,下面再講,先空著
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception { 
        // 1. 登陸登出設(shè)置
        http
        // 允許表單登陸
        .formLogin()
        // 自定義登陸頁面,注意action提交地址 和 賬號密碼表單name
//        .loginPage("/login.html")
        // 自定義后端登陸地址,security默認的是/login
//        .loginProcessingUrl("/doLogin")
        // 自定義登陸成功后的處理,前后端分離一般返回json數(shù)據(jù)
        .successHandler(new MyAuthenticationSuccessHandler())
        // 自定義登陸失敗后的處理,前后端分離一般返回json數(shù)據(jù)
        .failureHandler(new MyAuthenticationFailureHandler())
        .and()
        .logout()
        // 自定義退出地址
//        .logoutUrl("/logout")
        // 退出成功后的處理
        .logoutSuccessHandler((req,res,aut)->{
            res.setContentType("application/json;charset=utf-8");
            Result<String> result = new Result<>();
            result.setStatus(1);
            result.setCode("200");
            result.setMsg("退出成功");
            res.getWriter().write(JSONUtil.toJsonStr(result));
        })
        //使得session失效,默認true
//        .invalidateHttpSession(true)
        //清除認證信息,默認true
//        .clearAuthentication(true)
        //刪除指定的cookie
//        .deleteCookies("cookie01")
        ;
       
        // 2. 跨域問題
        http.csrf().disable();
        
        // 3. 權(quán)限設(shè)置
        http
            // 對url進行訪問權(quán)限控制
            .authorizeRequests() 
            // 按角色來控制權(quán)限的
            .antMatchers("/test/t2").hasRole("ADMIN")
            .antMatchers(
                    "/admin1/**",
                    "/admin2/**"
                    ).hasAnyRole("ADMIN1", "ADMIN2")
            // 按Authority,有權(quán)限才能訪問
            .antMatchers("/user/**").hasAuthority("/u/a")
            .antMatchers("/test/t1").hasAuthority("/test/t1")
            // 直接放行的
            .antMatchers("/app/**").permitAll()
            // 其他任何請求都需要登陸
            .anyRequest().authenticated()
            ;
    }

}
3.2.2 注解表達式配置權(quán)限

SpringSecurity的權(quán)限注解有5個,都是用在方法上的,分別是:

  • @Secured:檢查指定的角色權(quán)限,角色要加前綴ROLE_,可以多個,如:@Secured({"ROLE_A", "ROLE_B"})
  • @PreAuthorize:方法執(zhí)行前進行權(quán)限檢查,一般都是用這個。用法:@PreAuthorize("hasRole('admin')")、@PreAuthorize("hasAuthority('/t1') and hasAuthority('/t2')")@PreAuthorize("hasAnyRole('root','admin')")
  • @PostAuthorize:方法執(zhí)行后進行權(quán)限檢查,還沒用過
  • @PreFilter:過濾函數(shù),未用過
  • @PostFilter:過濾函數(shù),未用過

注意:

  • 要想使用注解,需要開啟注解,在security的配置類加上@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
    開啟注解
  • 注釋掉配置類方法中,關(guān)于權(quán)限的配置。

注解表達式使用如下:

package com.example.demo.controller;

import javax.servlet.http.HttpServletResponse;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/test")
public class TestController {
    
    @GetMapping("/t1")
    public String test1(String name, HttpServletResponse response) {
        response.addHeader("userId","123");
        return name == null?"zhangsan":name;
    }
    
    @GetMapping("/t2")
    @PreAuthorize("hasAnyRole('ROOT','ADMIN')")
    public String test2() {
        return "test2";
    }
    
    
    @GetMapping("/t3")
    @PreAuthorize("hasAuthority('/test/t3')")
    public String test3() {
        return "test3";
    }
    
    @GetMapping("/t4")
    @PreAuthorize("hasAuthority('/t4') and hasAuthority('/t5')")
    public String test4() {
        return "test4";
    }
    
    @GetMapping("/t5")
    @PreAuthorize("hasRole('admin')")
    public String test5() {
        return "test5";
    }

}

注解的判斷方法走的是類org.springframework.security.access.expression.SecurityExpressionRoot中的,可以看看邏輯。

?著作權(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)容