Spring Security的學習


Spring Security 正是 Spring 家族中的成員。Spring Security 基於 Spring 框架,提供了一套 Web 應用安全性的完整解決方案。如同Shiro一樣,安全框架最重要的就是用戶認證(Authentication)和用戶授權 (Authorization)兩個部分。實際上,在 Spring Boot 出現之前,Spring Security 就已經發展了多年了,但是使用的並不多,安全管理這個領域,一直是 Shiro 的天下。 相對於 Shiro,在 SSM 中整合 Spring Security 都是比較麻煩的操作,所以,Spring Security 雖然功能比 Shiro 強大,但是使用反而沒有 Shiro 多(Shiro 雖然功能沒有 Spring Security 多,但是對於大部分項目而言,Shiro 也夠用了)。 自從有了 Spring Boot 之后,Spring Boot 對於 Spring Security 提供了自動化配置方案,可以使用更少的配置來使用 Spring Security

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

引入依賴之后,啟動項目就可以訪問localhost:8080,默認的用戶名:user

密碼在項目啟動的時候在控制台會打印,注意每次啟動的時候密碼都回發生變化!

Spring Security 基礎類

基礎的Filter

SpringSecurity 采用的是責任鏈的設計模式,它本質是一條很長的過濾器鏈,這里有幾個比較重要,會在配置中使用到的Filter

  • FilterSecurityInterceptor:是一個方法級的權限過濾器, 基本位於過濾鏈的最底部,Spring MVC控制器的前面,正如其名Interceptor(攔截器)。雖然這個類名稱中帶有攔截器,但是它也實現了Filter接口。它會檢查前面過濾器鏈中是否已通過,通過之后才會調用后台服務
  • ExceptionTranslationFilter:是個異常過濾器,用來處理在認證授權過程中拋出的異常
  • UsernamePasswordAuthenticationFilter:對/login 的 POST 請求(可配置)做攔截,校驗表單中用戶名,密碼,在自定義認證Filter時就需要繼承這個類
  • BasicAuthenticationFilter:授權過濾器,會將用戶對應的權限列表放入Spring Security上下文(SecurityContextHolder.getContext().setAuthentication())中,在自定義認證Filter時就需要繼承這個類

UserDetailsService 接口

當什么也沒有配置的時候,賬號和密碼是由 Spring Security 定義生成的。而在實際項目中賬號和密碼都是從數據庫中查詢出來的。 所以我們要通過自定義邏輯控制認證邏輯。 如果需要自定義邏輯時,只需要實現 UserDetailsService 接口即可

這個接口只有一個方法(loadUserByUsername()),也就是根據用戶名獲取User,我們只需要重寫這個方法,根據參數(用戶名)去數據庫中查詢

UserDetails 接口

在上面loadUserByUsername()方法返回一個UserDetails,這個類在Spring Security中代表用戶主體,它定義的代碼如下

它有一個實現類org.springframework.security.core.userdetails.User,我們在重寫loadUserByUsername方法時就可以直接返回這個User對象(new User()),或者通過繼承User類重寫所需方法來定制UserDetails

PasswordEncoder 接口

這個接口用於密碼驗證,通過定義加密,解密方法可以完成用戶認證時的密碼處理。所以這個接口主要就是定義加密,解密方法

BCryptPasswordEncoSpringSecurity Web 權限方案der 是這個接口的一個實現類,也是Spring Security 官方推薦的密碼解析器,采用bcrypt 強散列加密實現。是基於 Hash 算法實現的單向加密算法。可以通過 strength 控制加密強度,默認 10

SpringSecurity Web 權限方案

設置登錄系統的賬號、密碼

方式一:在 application.properties配置文件中配置

spring.security.user.name=lz
spring.security.user.password=123

方式二:編寫配置類實現configure接口

在配置類定義的用戶名,密碼優先級高於配置文件中配置的

一個簡單的配置類

package com.lynu.config;

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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * 一個最簡單的配置類
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 創建一個基於BCrypt算法的密碼加密器(定義密碼加密器必不可少)
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        // 設置
        String password = passwordEncoder.encode("123");
        auth.inMemoryAuthentication().withUser("lz").password(password).roles("admin");
    }
}

方式三:在自定義配置類基礎上再實現userDetailsService接口查詢數據庫

方式一,方式二都是將用戶名,密碼硬編碼在配置文件或代碼中,在實際項目中肯定是要去查詢數據

一個較為全面的配置類

@Configuration
@EnableWebSecurity
// 開啟Spring Security注解支持
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // 注入自定義的userDetailsService去查詢數據庫
    @Autowired
    private UserDetailsService userDetailsService;

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

    // 設置密碼處理器
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    // 注入數據源用於rememberMe自動創建表以及將記住的用戶保存在數據在
    @Autowired
    private DataSource dataSource;
    // rememberMe配置
    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        // 賦值數據源
        jdbcTokenRepository.setDataSource(dataSource);
        // 自動創建所需要的表,第一次執行會創建,以后要執行就要刪除掉!
//        jdbcTokenRepository.setCreateTableOnStartup(true);
        return jdbcTokenRepository;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                    .loginPage("/login.html") // 自定義登錄頁面
                    .loginProcessingUrl("/user/login") // 登錄請求url
                    .defaultSuccessUrl("/success.html") // 登錄成功后跳轉url 同successForwardUrl方法
                    // .failureUrl("/login.html") // 登錄失敗后跳轉url 實際上不配置默認是回到登錄頁 同failureForwardUrl方法
                    // .usernameParameter("myUserName") // 指定登錄時用戶名參數名
                    // .passwordParameter("myPassword") // 指定登錄時密碼參數名
                    .permitAll()
                // 沒有權限時會到默認的403頁面,可以自定義403沒有權限錯誤提示頁面
                .and().exceptionHandling().accessDeniedPage("/unauth.html")
                // 設置退出url 退出后需要重新登錄認證
                .and().logout().logoutUrl("/logout").logoutSuccessUrl("/login.html").permitAll()
                // 配置認證與授權
                .and().authorizeRequests()
                    // 認證
                    .antMatchers("/", "/hello", "/user/login").permitAll() // 不需要認證的路徑
                    // 授權
                    // 基於權限訪問控制, 只有指定一個權限才可以訪問
//                    .antMatchers("/hello/index").hasAuthority("admin")
                    // 基於權限訪問控制, 只要有其中任意一個權限就可以訪問, 多個權限逗號分隔
//                    .antMatchers("/hello/index").hasAnyAuthority("admin,manager")
                    // 基於角色訪問控制, 只有指定的角色才可以訪問
//                    .antMatchers("/hello/index").hasRole("teacher")
                    // 基於角色訪問控制, 只要有任意一個角色就可以訪問, 多個權限逗號分隔
//                    .antMatchers("/hello/index").hasAnyRole("student,teacher")
                .anyRequest().authenticated() // 任何請求的url都需要認證
                // remember me
                .and().rememberMe().tokenRepository(persistentTokenRepository())
                    // 設置remember token有效時間 單位:秒
                    .tokenValiditySeconds(60)
                    .userDetailsService(userDetailsService)
                .and().csrf().disable(); // 關閉csrf防護
    }

}

自定義userDetailsService查詢數據庫

@Component("userDetailsService")
public class JdbcUserDetailsService implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    /**
    * 根據用戶名查詢數據庫
    */
    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        // 這里采用Mybatis-plus去查詢
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getUserName, userName);
        User user = userMapper.selectOne(queryWrapper);
        if (user == null) {
            throw new UsernameNotFoundException("用戶名不存在");
        }
        // 如果是基於權限進行訪問控制,需要給用戶賦予權限列表名,這里設置的是名為admin的權限
        // 如果是基於角色進行訪問控制 角色名固定為ROLE_xxx 可以查看SpringSecurity hasRole()源碼 會自動給hasRole()方法設置的角色加上ROLE_前綴
        // 如果設置角色后還是403 可以嘗試更換角色名 比如我設置ROLE_abc角色,hasRole("abc")就無法成功
        List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_teacher");
        return new org.springframework.security.core.userdetails.User(user.getUserName(), new BCryptPasswordEncoder().encode(user.getPassWord()), auths);
    }
}

頁面登錄認證

頁面提交方式必須為 post 請求,用戶名,密碼輸入框的name值必須為 username, password 原因: 在執行登錄的時候會走一個過濾器 UsernamePasswordAuthenticationFilter,在這個Filter中定義了默認的name值以及默認只接受post請求

如果需要用戶名,密碼輸入框的name值,可以在configure配置方法中通過 usernameParameter()和 passwordParameter()方法修改

基於角色或權限進行訪問控制

這里涉及到四個方法用於基於角色或權限進行訪問控制,每種方式各兩個方法,在基於注解的url訪問控制中使用的也是這四個方法

基於權限

  • hasAuthority 方法:設置用戶需要有哪一個權限才能訪問,如果當前的主體具有指定的權限就允許訪問,否則轉發到 403頁面
  • hasAnyAuthority 方法:如果當前的主體有任何提供的權限列表(給定一個逗號分隔的字符串列表)的話就允許訪問,否則轉發到 403頁面

基於角色

  • hasRole 方法 如果用戶具備給定角色就允許訪問,否則轉發到 403頁面
  • hasAnyRole 表示用戶具備任何一個角色都可以訪問,否則轉發到 403頁面

注意:在configure配置方法中不需要ROLE_前綴,但是在自定義的userDetailsService中需要加上ROLE_前綴,這是Spring Security源碼中定義的,所以要么在數據庫角色表中定義的角色名就加上前綴,要么在自定義的userDetailsService中通過代碼處理下給加上前綴

基於注解的訪問控制

使用Spring Security提供的注解就需要先開啟配置

@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
  • @Secured: 判斷是否具有角色,另外需要注意的是這里匹配的字符串需要添加前綴“ROLE_“

  • @PreAuthorize:注解適合進入方法前的權限驗證,只有給定的權限才能訪問,在這個注解中使用訪問控制中的四個方法

  • @PostAuthorize: 注解在方法執行后再進行權限驗證,適合驗證帶有返回值的方法進行訪問控制,在這個注解中使用訪問控制中的四個方法

  • @PreFilter: 進入控制器之前對數據進行過濾,此時filterObject表示的是方法入參List中元素id%2==0的參數才能傳入方法

  • @PostFilter:權限驗證之后對數據進行過濾,此時表達式中的 filterObject 引用的是方法返回值 List 中的某一個元素

更多在注解中可使用的表達式方法可以查看官方文檔:https://docs.spring.io/spring-security/site/docs/5.3.4.RELEASE/reference/html5/#el-common-built-in

基於數據庫的記住我

  1. 因為是基於數據庫的操作,所以關於數據庫的配置就不再贅述,需要先生成一張表,可以通過setCreateTableOnStartup(true)方法自動生成,也可以手動創建這張表,這Spring Security定義的表,可以從源碼中找到表的DDL
CREATE TABLE `persistent_logins` (
 `username` varchar(64) NOT NULL,
 `series` varchar(64) NOT NULL,
 `token` varchar(64) NOT NULL,
 `last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE 
CURRENT_TIMESTAMP,
 PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  1. 在配置類中加入如下代碼
// 注入數據源用於rememberMe自動創建表以及將記住的用戶保存在數據在
    @Autowired
    private DataSource dataSource;
    // rememberMe配置
    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        // 賦值數據源
        jdbcTokenRepository.setDataSource(dataSource);
        // 自動創建所需要的表,第一次執行會創建,以后要執行就要刪除掉!
//        jdbcTokenRepository.setCreateTableOnStartup(true);
        return jdbcTokenRepository;
    }
  1. 修改安全配置方法configure
// 開啟記住我功能
http.rememberMe()
 .tokenRepository(tokenRepository)
 .userDetailsService(usersService);

  1. 頁面定義一個復選框用於選中記住密碼,注意:name 屬性值必須是 remember-me,不能改為其他值

  2. 設置有效期,默認 2 周時間。但是可以通過設置狀態有效時間,即使項目重新啟動下次也可以正常登錄,還是修改安全配置方法configure

// 設置remember token有效時間 單位:秒
.tokenValiditySeconds(60)

用戶登出注銷

用戶在頁面上通過一個url進行登出注銷,在后端Spring Security中需要配置下

http.logout().logoutUrl("/logout").logoutSuccessUrl("/index").permitAll

CSRF 跨站請求偽造

CSRF是一種挾制用戶在當前已 登錄的 Web 應用程序上執行非本意的操作的攻擊方法。簡單地說,是攻擊者通過一些技術手段欺騙用戶的瀏覽器去訪問一個 自己曾經認證過的網站並運行一些操作(如發郵件,發消息,甚至財產操作如轉賬和購買 商品)。由於瀏覽器曾經認證過,所以被訪問的網站會認為是真正的用戶操作而去運行。 這利用了 web 中用戶身份驗證的一個漏洞:簡單的身份驗證只能保證請求發自某個用戶的 瀏覽器,卻不能保證請求本身是用戶自願發出的

從 Spring Security 4.0 開始,默認情況下會啟用 CSRF 保護,以防止 CSRF 攻擊應用 程序,Spring Security CSRF 會針對 PATCH,POST,PUT 和 DELETE 方法進行防護

如果開啟CSRF保護,就需要在每個表單請求中攜帶一個名為_csrf的隱藏域

<input type="hidden"th:if="${_csrf}!=null"th:value="${_csrf.token}"name="_csrf"/>

Spring Security 實現 CSRF是將自動生成的 csrfToken 保存到 HttpSession 或者 Cookie 中,具體實現可以參看HttpSessionCsrfTokenRepository,CookieCsrfTokenRepository類,當請求到來時,從請求中提取 csrfToken,和保存在Session或Cookie中的 csrfToken 做比較,進而判斷當前請求是否合法。這個判斷過程主要通過 CsrfFilter 過濾器來完成

SpringSecurity 前后端分離權限方案

在前后端分離結構中,后端基本上不再使用Session保存用戶狀態,而是用使用Token實現用戶的無狀態,同時保證接口的通用性

根據用戶信息使用jwt生成Token並響應給前端,前端請求在請求頭中攜帶Token,在Spring Security中獲取請求頭中的Token並解析出用戶信息,根據用戶信息從關系型數據庫或nosql中獲取權限列表,並由Spring Security給當前用戶賦權(認證與授權

在每個接口上可以使用注解對請求進行細粒度的訪問控制

核心配置類

核心配置類就是繼承 WebSecurityConfigurerAdapter 並注解 @EnableWebSecurity 的配置。這個配置指明了用戶名密碼的處理方式、請求路徑、登錄/登出控制等和安全相關的配置

package com.lynu.security.config;

import com.lynu.security.filter.TokenAuthFilter;
import com.lynu.security.filter.TokenLoginFilter;
import com.lynu.security.security.DefaultPasswordEncoder;
import com.lynu.security.security.TokenLogoutHandler;
import com.lynu.security.security.TokenManager;
import com.lynu.security.security.UnauthorizedEntryPoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter {

    // Token管理工具類(使用JWT生成Token, 或者從Token中獲取用戶信息)
    @Autowired
    private TokenManager tokenManager;
    // redis操作工具類 這里把用戶與其對應的權限列表保存在redis,也可以直接從關系型數據庫中獲取
    @Autowired
    private RedisTemplate redisTemplate;
    // 密碼管理工具類 這里使用MD5進行密碼加解密 也可以使用Spring security推薦的BCryptPasswordEncoder
    @Autowired
    private DefaultPasswordEncoder passwordEncoder;
    // 自定義查詢數據庫用戶名密碼和權限信息
    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.exceptionHandling()
                .authenticationEntryPoint(new UnauthorizedEntryPoint()) // 認證失敗處理類
                .and().csrf().disable()
                .authorizeRequests()
                .anyRequest().authenticated() // 所有請求都需要授權
                .and().logout().logoutUrl("/admin/security/index/logout").permitAll() // 登出處理url
                .addLogoutHandler(new TokenLogoutHandler(redisTemplate, tokenManager)).and() // 登出處理類
                // 配置認證filter
                .addFilter(new TokenLoginFilter(authenticationManager(), tokenManager, redisTemplate))
                // 配置授權filter
                .addFilter(new TokenAuthFilter(authenticationManager(), tokenManager, redisTemplate))
                .httpBasic();
    }

    // 處理userDetail(自定義 查詢數據庫登錄和權限列表)和密碼處理類
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
    }

    // 無需認證的url, 可以直接訪問
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().mvcMatchers("/api/**", "/swagger-ui.html/**");
    }
}

認證授權相關的工具類

DefaultPasswordEncoder(密碼管理工具類)

package com.lynu.security.security;

import com.lynu.util.utils.MD5;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

@Component
public class DefaultPasswordEncoder implements PasswordEncoder {
    @Override
    public String encode(CharSequence charSequence) {
        return MD5.encrypt(charSequence.toString());
    }

    @Override
    public boolean matches(CharSequence charSequence, String password) {
        return password.equals(MD5.encrypt(charSequence.toString()));
    }
}

TokenManager(Token管理工具類)

package com.lynu.security.security;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
public class TokenManager {

    // Token有效時長
    private static final long expirTime = 24 * 60 * 60 * 1000;
    // Token密鑰
    private static final String tokenSignKey = "123456";

    /**
     * 根據用戶名生成Token
     * @param userName 用戶名
     * @return
     */
    public String createToken(String userName) {
        return Jwts.builder()
                .setSubject(userName)
                .setExpiration(new Date(System.currentTimeMillis() + expirTime))
                .signWith(SignatureAlgorithm.HS256, tokenSignKey)
                .compact();
    }

    /**
     * 從Token中解析出用戶名
     * @param token Token字符串
     * @return
     */
    public String getUserNameFromToken(String token) {
        return Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJwt(token).getBody().getSubject();
    }

    /**
     * 移除Token
     * @param token 移除Token
     */
    public void removeToken(String token) {
		// 這里使用空方法模擬移除,也可以編寫代碼讓Token失效
    }

}

TokenLogoutHandler(登出處理類)

package com.lynu.security.security;

import com.lynu.util.utils.R;
import com.lynu.util.utils.ResponseUtil;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutHandler;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class TokenLogoutHandler implements LogoutHandler {

    private RedisTemplate redisTemplate;
    private TokenManager tokenManager;

    public TokenLogoutHandler(RedisTemplate redisTemplate, TokenManager tokenManager) {
        this.redisTemplate = redisTemplate;
        this.tokenManager = tokenManager;
    }

    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        // 從Header中獲取Token
        String token = request.getHeader("Token");
        if (token != null) {
            // 移除Token
            tokenManager.removeToken(token);
            // 從Redis中刪除Token
            redisTemplate.delete("");
        }
        // 通過響應流輸出
        ResponseUtil.out(response, R.ok());
    }
}

UnauthorizedEntryPoint(未授權統一處理類)

package com.lynu.security.security;

import com.lynu.util.utils.R;
import com.lynu.util.utils.ResponseUtil;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class UnauthorizedEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        ResponseUtil.out(response, R.error());
    }
}

ResoinseUtil(響應工具類)

package com.lynu.util.utils;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class ResponseUtil {

    public static void out(HttpServletResponse response, R r) {
        ObjectMapper mapper = new ObjectMapper();
        response.setStatus(HttpStatus.OK.value());
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        try {
            mapper.writeValue(response.getWriter(), r);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

創建認證授權實體類

SecurityUser(安全實體類)

實現了UserDetails接口,同時包含普通用戶實體User

package com.lynu.security.entity;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.StringUtils;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

@Data
@Slf4j
public class SecurityUser implements UserDetails {
    // 當前登錄用戶
    private transient User currentUserInfo;
    // 當前用戶所有的權限列表字符串集合
    private List<String> permissionValueList;
    
    public SecurityUser() {
    }
    
    public SecurityUser(User user) {
        if (user != null) {
            this.currentUserInfo = user;
        }
    }

    /**
     * 將當前用戶的權限列表封裝為Collection<? extends GrantedAuthority>類型
     * @return
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        for(String permissionValue : permissionValueList) {
            if(StringUtils.isEmpty(permissionValue)) continue;
            SimpleGrantedAuthority authority = new
                    SimpleGrantedAuthority(permissionValue);
            authorities.add(authority);
        }
        return authorities;
    }
    
    @Override
    public String getPassword() {
        return currentUserInfo.getPassword();
    }
    @Override
    public String getUsername() {
        return currentUserInfo.getUsername();
    }
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    @Override
    public boolean isEnabled() {
        return true;
    }

}

User(普通用戶實體類)

User對應業務相關,數據表相關的用戶實體,需按實際情況修改該類的字段

package com.lynu.security.entity;

import io.swagger.annotations.ApiModel;
import lombok.Data;

import java.io.Serializable;

@Data
@ApiModel(description = "用戶實體類")
public class User implements Serializable {
    private String username;
    private String password;
    private String nickName;
    private String salt;
    private String token;
}

創建認證和授權的 filter

TokenLoginFilter(認證的filter)

package com.lynu.security.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.lynu.security.entity.SecurityUser;
import com.lynu.security.entity.User;
import com.lynu.security.security.TokenManager;
import com.lynu.util.utils.R;
import com.lynu.util.utils.ResponseUtil;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;

public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {

    private TokenManager tokenManager;
    private RedisTemplate redisTemplate;
    private AuthenticationManager authenticationManager;

    public TokenLoginFilter(AuthenticationManager authenticationManager, TokenManager tokenManager, RedisTemplate redisTemplate) {
        this.tokenManager = tokenManager;
        this.redisTemplate = redisTemplate;
        this.authenticationManager = authenticationManager;
        // 只有POST請求才認證設置為false 認證所有請求方式
        this.setPostOnly(false);
        // 認證的請求url和請求方式
        this.setRequiresAuthenticationRequestMatcher(new
                AntPathRequestMatcher("/admin/security/login","POST"));
    }

    /**
     * 獲取認證提交的用戶名和密碼
     * 該方法首先被調用 調用之后會再調用UserDetail的loadUserByUsername()方法
     * @param request
     * @param response
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        // 獲取提交的數據轉換為User對象
        try {
            User user = new ObjectMapper().readValue(request.getInputStream(), User.class);
            // 用戶名 密碼 權限列表(這里先給一個空權限列表)
            return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(),
                    user.getPassword(), new ArrayList<>()));
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

    /**
     * 認證成功后調用的方法
     * @param request
     * @param response
     * @param chain
     * @param authResult
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        SecurityUser user = (SecurityUser) authResult.getPrincipal();
        // 把k:用戶名 v:權限列表放入redis
        redisTemplate.opsForValue().set(user.getUsername(), user.getPermissionValueList());
        // 根據user生成Token
        String token = tokenManager.createToken(user.getUsername());
        // 返回Token
        ResponseUtil.out(response, R.ok().data("token", token));
    }

    /**
     * 認證失敗后調用的方法
     * @param request
     * @param response
     * @param failed
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        // 響應失敗信息
        ResponseUtil.out(response, R.error());
    }
}

TokenAuthFilter(授權的filter)

package com.lynu.security.filter;

import com.lynu.security.security.TokenManager;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

public class TokenAuthFilter extends BasicAuthenticationFilter {

    private TokenManager tokenManager;
    private RedisTemplate redisTemplate;

    public TokenAuthFilter(AuthenticationManager authenticationManager, TokenManager tokenManager, RedisTemplate redisTemplate) {
        super(authenticationManager);
        this.tokenManager = tokenManager;
        this.redisTemplate = redisTemplate;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 獲取當前認證成功用戶的權限
        UsernamePasswordAuthenticationToken authToken = getAuthentication(request);
        // 如果權限不為空將權限放入Spring Security上下文
        if (authToken != null) {
            SecurityContextHolder.getContext().setAuthentication(authToken);
        }
    }

    // 根據請求頭獲取用戶名 根據用戶名獲取權限
    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
        String token = request.getHeader("token");
        if (token != null) {
            String userName = tokenManager.getUserNameFromToken(token);
            // 從redis中獲取權限列表
            List<String> permissionValueList = (List<String>) redisTemplate.opsForValue().get(userName);
            Collection<GrantedAuthority> authorities = new ArrayList<>();
            for (String permission : permissionValueList) {
                authorities.add(new SimpleGrantedAuthority(permission));
            }
            // 用戶名 token 權限列表
            return new UsernamePasswordAuthenticationToken(userName, token, authorities);
        }
        return null;
    }

}

UserDetailServiceImpl(自定義查詢數據的UserDetailsService)

package com.lynu.securityservice.config;

import com.lynu.securityservice.entity.User;
import com.lynu.securityservice.service.PermissionService;
import com.lynu.securityservice.service.UserService;
import com.lynu.security.entity.SecurityUser;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

import java.util.List;

@Component("userDetailsService")
public class UserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private UserService userService;
    @Autowired
    private PermissionService permissionService;

    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        // 根據用戶名查詢數據庫
        User user = userService.selectByUsername(userName);
        if (user == null) {
            throw new UsernameNotFoundException("用戶不存在");
        }
        // 這里是因為DO對象是com.lynu.securityservice.entity.User, 而SecurityUser中對象是com.lynu.security.entity.User, 所以需要copy,如果兩個對象一致可以省略這步驟
        com.lynu.security.entity.User curUser = new com.lynu.security.entity.User();
        BeanUtils.copyProperties(user, curUser);
        // 根據用戶Id查詢權限
        List<String> permissionList = permissionService.selectPermissionValueByUserId(user.getId());
        // 根據當前用戶和權限列表構建UserDetails
        SecurityUser securityUser = new SecurityUser(curUser);
        securityUser.setPermissionValueList(permissionList);
        return securityUser;
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM