springSecurity非前后端分離配置(生產(chǎn)環(huán)境可用)

1 場(chǎng)景

1.1 說明

springSecurity作為一個(gè)權(quán)限管理系統(tǒng),在生產(chǎn)環(huán)境使用,還是比較復(fù)雜的,涉及的相關(guān)點(diǎn)比較多。網(wǎng)上文章里,并沒有比較全的配置。

本文主要將springSecurity在生產(chǎn)環(huán)境中使用時(shí),需要注意到的地方,進(jìn)行了相關(guān)整理。

springSecurity默認(rèn)是基于session的非前后端分離場(chǎng)景,本文基于此場(chǎng)景進(jìn)行配置,有時(shí)間的時(shí)候,后續(xù)會(huì)記錄補(bǔ)充如下場(chǎng)景:
(1)非前后端分離
(2)基于JWT的非前后端分離
(3)網(wǎng)關(guān)上整合權(quán)限控制

1.2 源碼

本文是基于各個(gè)應(yīng)用模塊單獨(dú)寫的配置,完整的測(cè)試代碼,可關(guān)注、點(diǎn)贊后私信博主。所有的代碼,博主均已校驗(yàn)過,demo可正常跑通,可直接應(yīng)用到生產(chǎn)環(huán)境中。

1.3 版本

spring-boot版本:2.3.3.RELEASE

其他版本:

<!-- ==========【freemarker權(quán)限security標(biāo)簽支持】========== start -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-taglibs</artifactId>
    <version>5.3.4.RELEASE</version>
</dependency>
<dependency>
    <groupId>javax.servlet.jsp</groupId>
    <artifactId>javax.servlet.jsp-api</artifactId>
    <version>2.3.3</version>
    <scope>provided</scope>
</dependency>
<!-- ==========【freemarker權(quán)限security標(biāo)簽支持】========== end -->

2 登錄

前提,屏蔽csrf防護(hù),否則系統(tǒng)登錄、登出無法正常訪問。如需打開,需自己進(jìn)行相關(guān)配置。

// 屏蔽csrf防護(hù)
http.csrf().disable();

2.1 自定義登錄頁面

本文前端頁面,使用的freemarker。

2.1.1 頁面配置

springSecurity登錄表單默認(rèn)使用的是自己的頁面,一般系統(tǒng)都需要進(jìn)行自定義登錄頁面。

訪問系統(tǒng)頁面時(shí),如未認(rèn)證,則自動(dòng)跳轉(zhuǎn)到登錄頁面

文件路徑:resources\templates\system\main.ftlh

如下:

<form action="/doLogin" method="post">
    <table>
        <tr>
            <td>用戶名:</td>
            <td><input type="text" name="username" value="admin"></td>
        </tr>
        <tr>
            <td>密碼:</td>
            <td><input type="text" name="password" value="123456"></td>
        </tr>
        <tr>
            <td><input type="submit" value="登錄"></td>
        </tr>
    </table>
</form>

如上代碼所示,登錄時(shí)的相關(guān)參數(shù)如下:

參數(shù)描述 參數(shù)
加載登錄頁面路徑 /initLogin
提交登錄請(qǐng)求路徑 /doLogin
用戶名 username
密碼 password
2.1.2 后臺(tái)代碼配置
/**
  * 加載登錄頁面
  * @return
  */
@RequestMapping(value = {"initLogin"})
public ModelAndView initLogin(HttpServletRequest request) {
    ModelAndView modelAndView = new ModelAndView("system/login");
    return modelAndView;
}

備注:提交登錄請(qǐng)求路徑(/doLogin)無需配置,此請(qǐng)求,會(huì)走security自己的認(rèn)證流程。

2.1.3 登錄請(qǐng)求放行

放行“登錄頁面”,“登錄請(qǐng)求”等相關(guān)權(quán)限驗(yàn)證請(qǐng)求

http.authorizeRequests()
    // ......
    // 放行“登錄頁面”,“登錄請(qǐng)求”,“退出”等相關(guān)權(quán)限驗(yàn)證請(qǐng)求
    .antMatchers("/initLogin", "/doLogin", "/doLogout").permitAll()
    // 任意請(qǐng)求需認(rèn)證通過
    .anyRequest().authenticated();
2.1.4 登錄參數(shù)配置

security的登錄請(qǐng)求路徑和參數(shù)都是默認(rèn)配置的,這里我們更改為自己的請(qǐng)求配置:

http.formLogin()
    // 登錄時(shí)自定義“用戶”參數(shù)名(默認(rèn)為:username)
    .usernameParameter("username")
    // 登錄時(shí)自定義“用戶”參數(shù)名(默認(rèn)為:password)
    .passwordParameter("password")
    // 自定義登錄頁面(默認(rèn)為:login/GET)
    .loginPage("/initLogin")
    // 自定義登錄請(qǐng)求路徑(默認(rèn)為:login/POST)
    .loginProcessingUrl("/doLogin")

2.2 登錄校驗(yàn)邏輯

2.1 自定義登錄用戶對(duì)象

擴(kuò)展security自帶的用戶對(duì)象(org.springframework.security.core.userdetails.User),擴(kuò)展自定義屬性

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.util.Collection;

/**
 * 自定義用戶對(duì)象
 */
public class LoginUser extends User {
    
    /**
     * 自定義用戶屬性
     */
    private String departmentCode;
    
    public LoginUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
        super(username, password, authorities);
    }
    
    public String getDepartmentCode() {
        return departmentCode;
    }
    
    public void setDepartmentCode(String departmentCode) {
        this.departmentCode = departmentCode;
    }
}
2.2 自定義用戶認(rèn)證service

自定義用戶認(rèn)證,主要是將用戶根據(jù)用戶名,從數(shù)據(jù)庫中查詢出來。組裝好org.springframework.security.core.userdetails.UserDetails的實(shí)現(xiàn)類對(duì)象,交由security進(jìn)行驗(yàn)證。后續(xù)security的驗(yàn)證,如未拋出異常,則認(rèn)證通過,否則認(rèn)證失敗,然后,可根據(jù)拋出的異常類型,來識(shí)別認(rèn)證失敗的原因。

/**
 * 自定義用戶認(rèn)證service
 */
@Component
public class CustomUserDetailsService implements UserDetailsService {
    
    @Resource
    private UserService userService;
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // ========== 【1】校驗(yàn)參數(shù) ========== 
        if (StringUtils.isBlank(username)) {
            throw new UsernameNotFoundException("用戶代碼為空");
        }
        
        // ========== 【2】查詢用戶信息 ========== 
        UserInfo userInfo = userService.getUserInfoByUserName(username);
        if (userInfo == null) {
            throw new UsernameNotFoundException("用戶名或密碼錯(cuò)誤");
        }
        // 密碼
        String password = userInfo.getPassword();
        // 用戶其他信息(用戶部門代碼)
        String departmentCode = userInfo.getDepartmentCode();
        
        // ========== 【3】查詢授權(quán)信息 ========== 
        // 角色代碼和授權(quán)代碼,均在此列表中配置(角色代碼前加“ROLE_”,來和權(quán)限區(qū)分)
        List<String> authorityList = new ArrayList<>();
        // 初始化角色信息
        List<String> roleCodeList = userService.getRoleCodeListByUserName(username);
        if (CollectionUtils.isNotEmpty(roleCodeList)) {
            for (String roleCode : roleCodeList) {
                authorityList.add("ROLE_" + roleCode);
            }
        }
        // 初始化權(quán)限信息
        List<String> permissionCodeList = userService.getPermissionCodeListByUserName(username);
        if (CollectionUtils.isNotEmpty(permissionCodeList)) {
            authorityList.addAll(permissionCodeList);
        }
        
        // ========== 【4】組裝用戶信息 ==========
        // 組裝通用信息
        LoginUser loginUser = new LoginUser(username, password, AuthorityUtils.createAuthorityList(authorityList.toArray(new String[0])));
        // 組裝自定義用戶信息
        loginUser.setDepartmentCode(departmentCode);
        
        return loginUser;
    }
}

2.3 認(rèn)證通過邏輯

認(rèn)證通過后,需進(jìn)行頁面跳轉(zhuǎn),有兩種方式,一種是跳轉(zhuǎn)到訪問登錄頁面前的頁面(訪問某個(gè)頁面,因?yàn)槲凑J(rèn)證,自動(dòng)跳轉(zhuǎn)到登錄頁面,當(dāng)?shù)卿洺晒?,自?dòng)跳轉(zhuǎn)到此頁面,而不是系統(tǒng)主頁面),一種是跳轉(zhuǎn)到系統(tǒng)主頁面。

可根據(jù)實(shí)際業(yè)務(wù)需求選擇,這里選擇第一種。

一般登錄成功后,系統(tǒng)會(huì)執(zhí)行自定義邏輯,如記錄登錄IP、登錄時(shí)間等,這里使用自定義登錄成功邏輯。

2.3.1 自定義登錄成功處理器
/**
 * 自定義登錄成功處理器
 */
@Component
public class CustomLoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
    
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
        // TODO 自定義登錄成功邏輯......
        System.out.println("自定義登錄成功邏輯......");
        super.onAuthenticationSuccess(request, response, authentication);
    }
}
2.3.2 配置登錄成功邏輯
  • 注入bean
/**
  * 自定義登錄成功邏輯
 */
@Autowired
private CustomLoginSuccessHandler customLoginSuccessHandler;
  • 配置執(zhí)行器
http.formLogin()
    // 自定義登錄成功forward路徑
    //.successForwardUrl("/")
    // 自定義登錄成功redirect路徑【默認(rèn)】(登錄成功后,頁面重定向到跳轉(zhuǎn)登錄頁面前的頁面Referer)
    //.defaultSuccessUrl("/")
    // 自定義登錄成功邏輯(redirect主頁面+自定義業(yè)務(wù)邏輯)
    .successHandler(customLoginSuccessHandler)
    // 自定義登錄成功redirect路徑(登錄成功后,頁面重定向到設(shè)置的登錄成功頁面:"/")
    //.defaultSuccessUrl("/", true)
2.3.3 登錄主頁面后臺(tái)代碼
/**
  * 系統(tǒng)主頁面
  * @return
  */
@RequestMapping(value = {"/"})
public ModelAndView main() {
    ModelAndView modelAndView = new ModelAndView("system/main");

    // 獲取當(dāng)前登錄人信息
    LoginUser loginUser = SecurityUtils.getLoginUser();
    modelAndView.addObject("loginUser", loginUser);

    return modelAndView;
}

2.4 獲取認(rèn)證信息

security認(rèn)證通過后,會(huì)將認(rèn)證信息,保存在ThreadLocal中,故可以通過其自帶的靜態(tài)方法獲?。?/p>

SecurityContextHolder.getContext().getAuthentication()

此處可獲取2.2中封裝的自定義對(duì)象:LoginUser。

/**
 * security工具類
 */
public class SecurityUtils {
    
    /**
     * 默認(rèn)角色前綴
     */
    private static final String DEFAULT_ROLE_PREFIX = "ROLE_";
    
    /**
     * 獲取認(rèn)證信息
     * @return
     */
    public static Authentication getAuthentication() {
        SecurityContext securityContext = SecurityContextHolder.getContext();
        if (securityContext != null) {
            return securityContext.getAuthentication();
        }
        return null;
    }
    
    /**
     * 獲取當(dāng)前登錄用戶對(duì)象
     * @return
     */
    public static LoginUser getLoginUser() {
        Authentication authentication = SecurityUtils.getAuthentication();
        if (authentication != null) {
            return (LoginUser) authentication.getPrincipal();
        }
        return null;
    }
}

2.5 認(rèn)證失敗邏輯

認(rèn)證失敗時(shí),跳轉(zhuǎn)到登錄頁面,并返回錯(cuò)誤信息。

這里我們通過forward到認(rèn)證失敗頁面(即登錄頁面),由于使用的是forward進(jìn)行的跳轉(zhuǎn),故可以獲取request中的屬性WebAttributes.AUTHENTICATION_EXCEPTION,來獲取異常信息,來返回到前臺(tái)。

也可通過redirect到認(rèn)證失敗頁面(登錄頁面),但是請(qǐng)求中無法獲取失敗異常信息WebAttributes.AUTHENTICATION_EXCEPTION。
可以考慮自定義認(rèn)證失敗邏輯failureHandler,來實(shí)現(xiàn)此功能。

2.5.1 認(rèn)證失敗后臺(tái)
/**
  * 登錄失敗頁面
  * @param request
  * @return
  */
@RequestMapping(value = {"loginFail"})
public ModelAndView loginFail(HttpServletRequest request) {
    ModelAndView modelAndView = new ModelAndView("system/login");
    String error = null;
    // 登錄異常處理
    Object exception = request.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
    if (exception instanceof AuthenticationException) {
        if(exception instanceof UsernameNotFoundException){
            // 自己拋出異常信息
            error = ((UsernameNotFoundException) exception).getMessage();
        }else if (exception instanceof BadCredentialsException){
            error = "用戶名或者密碼輸入錯(cuò)誤,請(qǐng)重新輸入!";
        }else if (exception instanceof LockedException){
            error = "賬戶被鎖定,請(qǐng)聯(lián)系管理員!";
        }else if (exception instanceof CredentialsExpiredException){
            error = "密碼過期,請(qǐng)聯(lián)系管理員!";
        }else if (exception instanceof AccountExpiredException){
            error = "賬戶過期,請(qǐng)聯(lián)系管理員!";
        }else if (exception instanceof DisabledException){
            error = "賬戶被禁用,請(qǐng)聯(lián)系管理員!";
        }else{
            error = "認(rèn)證失敗";
        }
    }
    modelAndView.addObject("error", error);
    return modelAndView;
}
2.5.2 認(rèn)證失敗配置
http.formLogin()
    // 自定義失敗forward路徑(默認(rèn)為:loginPage + "?error")
    .failureForwardUrl("/loginFail")

2 登出

2.1 頁面配置

<a href="/doLogout">退出</a>

2.2 后臺(tái)代碼

登出,走的是security的邏輯,無需自己寫后臺(tái)代碼。

2.3 登出請(qǐng)求放行

有可能執(zhí)行登出操作的時(shí)候,session已失效,因此登出系統(tǒng)也需要放行請(qǐng)求,不進(jìn)行認(rèn)證校驗(yàn)

http.authorizeRequests()
    // ......
    // 放行“登錄頁面”,“登錄請(qǐng)求”,“退出”等相關(guān)權(quán)限驗(yàn)證請(qǐng)求
    .antMatchers("/initLogin", "/doLogin", "/doLogout").permitAll()
    // 任意請(qǐng)求需認(rèn)證通過
    .anyRequest().authenticated();

2.4 自定義登出處理器

一般系統(tǒng)登出時(shí),也會(huì)進(jìn)行相關(guān)業(yè)務(wù)操作,如記錄日志,發(fā)送消息等。

2.4.1 自定義登出成功處理器
/**
 * 自定義登出成功處理器
 **/
@Configuration
public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        // TODO 自定義登出成功邏輯......
        System.out.println("自定義登出成功邏輯......");
        super.onLogoutSuccess(request, response, authentication);
    }
}
2.4.2 配置登錄成功邏輯
  • 注入bean
/**
  * 自定義登出成功邏輯
  */
@Autowired
private CustomLogoutSuccessHandler customLogoutSuccessHandler;
  • 配置執(zhí)行器
// ---------- [登出系統(tǒng)] ----------
http.logout()
    // 刪除認(rèn)證信息(默認(rèn)為true)
    .clearAuthentication(true)
    // 退出系統(tǒng)時(shí)使session無效(默認(rèn)為true)
    .invalidateHttpSession(true)
    // 退出系統(tǒng)時(shí),自定義請(qǐng)求路徑
    .logoutUrl("/doLogout")
    // 自定義登出成功邏輯(redirect登錄頁面+自定義業(yè)務(wù)邏輯)
    .logoutSuccessHandler(customLogoutSuccessHandler);

3 URL授權(quán)驗(yàn)證

security可以對(duì)指定URL進(jìn)行授權(quán)驗(yàn)證判斷。個(gè)人認(rèn)為主要是用來對(duì)某些特殊訪問請(qǐng)求進(jìn)行專門的認(rèn)證(如api),一般不會(huì)在此配置角色權(quán)限的驗(yàn)證。

3.1 驗(yàn)證配置
// ---------- [基于表單的身份驗(yàn)證] ----------
// 順序很重要,從上而下依次驗(yàn)證
http.authorizeRequests()
    // ---------- 自定義基于URL授權(quán)驗(yàn)證[也可加http方式限制:(HttpMethod method, String... antPatterns)]
    // 判斷是否有某權(quán)限
    .antMatchers("/noPower/**").hasAuthority("noPower")
    // 判斷是否有某角色
    .antMatchers("/admin/**").hasRole("admin")
    // 判斷是否有任一權(quán)限
    .antMatchers("/user/list").hasAnyAuthority("user:list", "user:all")
    // 自定義權(quán)限校驗(yàn)(參數(shù)來自WebSecurityExpressionRoot屬性)
    .antMatchers("/api/**").access("@customAccessForApi.hasPermission(request,authentication)")
    // 放行“登錄”,“退出”等相關(guān)權(quán)限驗(yàn)證請(qǐng)求
    .antMatchers("/initLogin", "/doLogin", "/doLogout").permitAll()
    // 任意請(qǐng)求需認(rèn)證通過
    .anyRequest().authenticated();

認(rèn)證通過對(duì)請(qǐng)求進(jìn)行攔截,如antMatchers,可以使用多種認(rèn)證方式:

  • 使用hasRole對(duì)角色進(jìn)行認(rèn)證

  • 可以使用hasAuthority對(duì)權(quán)限認(rèn)證。

  • 通過自定義邏輯對(duì)請(qǐng)求進(jìn)行進(jìn)行認(rèn)證

3.2 自定義邏輯驗(yàn)證

3.2.1 自定義驗(yàn)證邏輯
/**
 * 自定義權(quán)限監(jiān)測(cè)
 */
@Component
public class CustomAccessForApi {
    /**
     * 權(quán)限監(jiān)測(cè)
     */
    public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
        if (StringUtils.startsWith(request.getRemoteAddr(), "192.168.56")) {
            // 滿足條件的IP,可以訪問此接口
            return true;
        }
        return false;
    }
}
3.2.2 配置自定義驗(yàn)證邏輯
 http.authorizeRequests()
     // 自定義權(quán)限校驗(yàn)(參數(shù)來自WebSecurityExpressionRoot屬性)
     .antMatchers("/api/**").access("@customAccessForApi.hasPermission(request,authentication)")

4 方法授權(quán)驗(yàn)證

security可以對(duì)具體某個(gè)方法進(jìn)行授權(quán)驗(yàn)證判斷,一般加在Controller的對(duì)外請(qǐng)求方法上。

此方法,需要配置注解@EnableGlobalMethodSecurity開啟方法前后權(quán)限判斷,一般使用@PreAuthorize進(jìn)行方法執(zhí)行前判斷

// 開啟全局“方法安全”控制(開啟方法前后權(quán)限判斷,一般使用@PreAuthorize進(jìn)行方法執(zhí)行前判斷)
@EnableGlobalMethodSecurity(prePostEnabled = true)

4.1 判斷是否有角色

@PreAuthorize("hasRole('admin')")
@ResponseBody
@RequestMapping("add")
public String add() {
    return "department add ...";
}

4.2 判斷是否有權(quán)限

@PreAuthorize("hasAuthority('department:list')")
@RequestMapping("list")
public ModelAndView list() {
    return new ModelAndView("department/department_list");
}

4.3 自定義權(quán)限判斷

4.3.1 自定義校驗(yàn)器
@Configuration
public class CustomAccessForDepartmentDelete {
    public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
        return false;
    }
}
4.3.2 配置自定義校驗(yàn)器
// 自定義權(quán)限控制,參數(shù)前需加#
@PreAuthorize("@customAccessForDepartmentDelete.hasPermission(#request,#authentication)")
@ResponseBody
@RequestMapping("delete")
public String delete() {
    return "department delete ...";
}

5 前端頁面授權(quán)驗(yàn)證

security也可以在前端頁面,加標(biāo)簽,來控制頁面元素的展示。這里前端使用的是freemarker,需要額外做些配置,才可以使用。

5.1 freemarker配置security標(biāo)簽

5.1.1 maven依賴
<!-- ==========【freemarker權(quán)限security標(biāo)簽支持】========== start -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-taglibs</artifactId>
    <version>5.3.4.RELEASE</version>
</dependency>
<dependency>
    <groupId>javax.servlet.jsp</groupId>
    <artifactId>javax.servlet.jsp-api</artifactId>
    <version>2.3.3</version>
    <scope>provided</scope>
</dependency>
<!-- ==========【freemarker權(quán)限security標(biāo)簽支持】========== end -->
5.1.2 拷貝tld文件

spring-security-taglibs/META-INF/security.tld,拷貝到拷貝到resource的目錄tags中。

5.1.3 配置
/**
 * Freemarker的Security標(biāo)簽支持
 */
@Configuration
public class FreemarkerSecurityTaglibConfig{
    
    /**
     * security標(biāo)簽路徑(來自"spring-security-taglibs/META-INF/security.tld")<br>
     * 此文件需拷貝到resource的目錄tags中
     */
    private static final String SECURITY_TLD_PATH="/tags/security.tld";
    
    @Autowired
    private FreeMarkerConfigurer freeMarkerConfigurer;
    
    @PostConstruct
    public void freeMarkerConfigurer() {
        List<String> classpathTlds = new ArrayList<>();
        classpathTlds.add(SECURITY_TLD_PATH);
        freeMarkerConfigurer.getTaglibFactory().setClasspathTlds(classpathTlds);
    }
}

5.2 引入標(biāo)簽

freemarker前臺(tái)文件ftlh,文件頭,引入標(biāo)簽:

<#assign security=JspTaglibs["http://www.springframework.org/security/tags"] />

5.3 使用

5.3.1 判斷有無角色
<@security.authorize access="hasRole('admin')">
    <a href="/department/test">test按鈕</a>
</@security.authorize>
5.3.2 判斷有無權(quán)限
<@security.authorize access="hasAuthority('department:test')">
    <a href="/department/test">test按鈕</a>
</@security.authorize>

6 api驗(yàn)證授權(quán)

有時(shí)候,我們希望在java代碼中直接判斷有無某角色、有無某權(quán)限。作者對(duì)此進(jìn)行了代碼封裝,可通過靜態(tài)方法進(jìn)行判斷。

此判斷,暫不支持權(quán)限繼承。

6.1 封裝

/**
 * security工具類
 */
public class SecurityUtils {
    
    /**
     * 默認(rèn)角色前綴
     */
    private static final String DEFAULT_ROLE_PREFIX = "ROLE_";
    
    /**
     * 獲取認(rèn)證信息
     * @return
     */
    public static Authentication getAuthentication() {
        SecurityContext securityContext = SecurityContextHolder.getContext();
        if (securityContext != null) {
            return securityContext.getAuthentication();
        }
        return null;
    }
    
    /**
     * 判斷有某權(quán)限(暫不支持權(quán)限繼承)
     * @return
     */
    public static boolean hasAuthority(String authority) {
        if (StringUtils.isNotEmpty(authority)) {
            return SecurityUtils.hasAnyAuthorityName(null, authority);
        }
        return false;
    }
    
    /**
     * 判斷有任一權(quán)限(暫不支持權(quán)限繼承)
     * @param authorityArr
     * @return
     */
    public static boolean hasAnyAuthority(String... authorityArr) {
        if (authorityArr != null && authorityArr.length > 0) {
            return SecurityUtils.hasAnyAuthorityName(null, authorityArr);
        }
        return false;
    }
    
    /**
     * 判斷有某角色(暫不支持權(quán)限繼承)
     * @param role
     * @return
     */
    public static boolean hasRole(String role) {
        if (StringUtils.isNotEmpty(role)) {
            return SecurityUtils.hasAnyAuthorityName(DEFAULT_ROLE_PREFIX, role);
        }
        return false;
    }
    
    /**
     * 判斷有任一角色(暫不支持權(quán)限繼承)
     * @param roleArr
     * @return
     */
    public static boolean hasAnyRole(String... roleArr) {
        if (roleArr != null && roleArr.length > 0) {
            return SecurityUtils.hasAnyAuthorityName(DEFAULT_ROLE_PREFIX, roleArr);
        }
        return false;
    }
    
    /**
     * 判斷是否滿足通用權(quán)限信息,有一個(gè)滿足即為滿足(包括角色和權(quán)限)
     * @param prefix         前綴
     * @param authorityNames 權(quán)限名稱
     * @return
     */
    private static boolean hasAnyAuthorityName(String prefix, String... authorityNames) {
        if (authorityNames != null && authorityNames.length > 0) {
            Set<String> authoritySet = SecurityUtils.getAuthoritySet();
            if (CollectionUtils.isNotEmpty(authoritySet)) {
                for (String authorityName : authorityNames) {
                    String defaultedRole = getRoleWithDefaultPrefix(prefix, authorityName);
                    if (authoritySet.contains(defaultedRole)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }
    
    /**
     * 獲取當(dāng)前用戶權(quán)限集合信息
     * @return
     */
    private static Set<String> getAuthoritySet() {
        Authentication authentication = SecurityUtils.getAuthentication();
        if (authentication != null) {
            Collection<? extends GrantedAuthority> userAuthorities = authentication.getAuthorities();
            return AuthorityUtils.authorityListToSet(userAuthorities);
        }
        return null;
    }
    
    /**
     * 如果defaultRolePrefix為非空且role的開頭不是defaultRolePrefix,則使用defaultRolePrefix前綴角色。
     * @param defaultRolePrefix
     * @param role
     * @return
     */
    private static String getRoleWithDefaultPrefix(String defaultRolePrefix, String role) {
        if (role == null) {
            return role;
        }
        if (defaultRolePrefix == null || defaultRolePrefix.length() == 0) {
            return role;
        }
        if (role.startsWith(defaultRolePrefix)) {
            return role;
        }
        return defaultRolePrefix + role;
    }
}

6.2 使用

java代碼中直接使用靜態(tài)方法判斷即可,如下:

// 判斷有角色
SecurityUtils.hasRole("admin");
// 判斷有任一角色
SecurityUtils.hasAnyRole("admin","user");
// 判斷有權(quán)限
SecurityUtils.hasAuthority("user:add");
// 判斷有任一權(quán)限
SecurityUtils.hasAnyAuthority("user:add","user:edit");

7 自定義授權(quán)驗(yàn)證失敗返回方式

當(dāng)系統(tǒng)訪問認(rèn)知失敗時(shí),默認(rèn)返回授權(quán)驗(yàn)證的錯(cuò)誤頁面,這種方式對(duì)于ajax的請(qǐng)求,十分不友好。

因此我們希望在驗(yàn)證授權(quán)失敗時(shí),如果是json請(qǐng)求,則返回json格式的錯(cuò)誤信息,如果是其他請(qǐng)求,則返回錯(cuò)誤頁面。

7.1 前臺(tái)頁面

授權(quán)驗(yàn)證失敗頁面accessDenied.ftlh

<body>
無訪問權(quán)限
</body>

7.2 后臺(tái)代碼

/**
  * 無訪問權(quán)限頁面
  * @return
  */
@RequestMapping("accessDenied")
public ModelAndView accessDenied() {
    return new ModelAndView("system/accessDenied");
}

7.3 配置

// ---------- [異常處理]ExceptionTranslationFilter ----------
http.exceptionHandling()
    // 認(rèn)證失?。ú皇褂媚J(rèn)的表單form登錄認(rèn)證時(shí),可使用此方式)
    //.authenticationEntryPoint((request, response, authException) -> {
    //})
    // 訪問拒絕句柄(認(rèn)證通過后,無操作權(quán)限時(shí))
    .accessDeniedHandler((request, response, accessDeniedException) -> {
        String contentType = request.getHeader("content-type");
        boolean jsonRequestFlag = (contentType != null && contentType.contains("json"));
        // 判斷是否是json請(qǐng)求
        if (jsonRequestFlag) {
            response.setStatus(HttpStatus.FORBIDDEN.value());
            response.setContentType("application/json;charset=UTF-8");
            Writer writer = response.getWriter();
            JSONObject json = new JSONObject();
            json.put("success", false);
            json.put("message", "無操作權(quán)限:" + accessDeniedException.getMessage());
            writer.write(json.toJSONString());
            writer.flush();
            writer.close();
        } else {
            request.getRequestDispatcher("/accessDenied").forward(request, response);
        }
    });

8 分布式session共享

可使用spring-session或tomcat-redis-session-manager。后續(xù)補(bǔ)充

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

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

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