Spring Security實現(xiàn)基于角色的訪問控制框架
說明
Spring Security是一個功能強大且高度可定制的身份驗證和訪問控制框架。Spring Security是一個專注于為Java應(yīng)用程序提供身份驗證和授權(quán)的框架。與所有Spring項目一樣,Spring安全性的真正威力在于它可以很容易地擴展以滿足定制需求。
一般Web應(yīng)用的需要進行認證和授權(quán)。
- 用戶認證(Authentication):驗證當前訪問系統(tǒng)的是不是本系統(tǒng)的用戶,并且要確認具體是哪個用戶。
- 用戶授權(quán)(Authorization):經(jīng)過認證后判斷當前用戶是否有權(quán)限進行某個操作。在一個系統(tǒng)中,不同用戶所具有的權(quán)限是不同的。
Spring Security與Shiro的區(qū)別
Spring Security 是 Spring家族中的一個安全管理框架。相比與另外一個安全框架Shiro,它提供了更豐富的功能,社區(qū)資源也比Shiro豐富。相對于Shiro,在SSH/SSM中整合Spring Security都是比較麻煩的操作,但有了Spring boot之后,Spring Boot 對于 Spring Security 提供了 自動化配置方案,可以零配置使用 Spring Security。因此,一般來說常見的安全管理技術(shù)棧組合是:
- SSM+Shiro
- Spring Boot /Spring Clound +Spring Security
簡單使用
登錄校驗流程

引入Security
在SpringBoot項目中使用SpringSecurity只需要引入依賴即可。
<!-- spring security 安全認證 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
設(shè)置用戶名和密碼
在application.properties中添加屬性:
server.port=8080 #spring.security.user.name=root #spring.security.user.password=123456 #mysql數(shù)據(jù)庫連接 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://localhost:3306/security?serverTimezone=GMT%2B8 spring.datasource.username=root spring.datasource.password=123456
但是在application.properties中添加屬性意味著登錄系統(tǒng)的用戶名的密碼都是固定的,不推薦。可以使用配置類,在配置類中設(shè)置,配置類的優(yōu)先級更高。
使用配置類
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
/**
* 自定義用戶認證邏輯
*/
@Autowired
private UserDetailsService userDetailsService;
/**
* 認證失敗處理類
*/
@Autowired
private AuthenticationEntryPointImpl unauthorizedHandler;
/**
* 退出處理類
*/
@Autowired
private LogoutSuccessHandlerImpl logoutSuccessHandler;
/**
* token認證過濾器
*/
@Autowired
private JwtAuthenticationTokenFilter authenticationTokenFilter;
/**
* 解決 無法直接注入 AuthenticationManager
*
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception
{
return super.authenticationManagerBean();
}
/**
* anyRequest | 匹配所有請求路徑
* access | SpringEl表達式結(jié)果為true時可以訪問
* anonymous | 匿名可以訪問
* denyAll | 用戶不能訪問
* fullyAuthenticated | 用戶完全認證可以訪問(非remember-me下自動登錄)
* hasAnyAuthority | 如果有參數(shù),參數(shù)表示權(quán)限,則其中任何一個權(quán)限可以訪問
* hasAnyRole | 如果有參數(shù),參數(shù)表示角色,則其中任何一個角色可以訪問
* hasAuthority | 如果有參數(shù),參數(shù)表示權(quán)限,則其權(quán)限可以訪問
* hasIpAddress | 如果有參數(shù),參數(shù)表示IP地址,如果用戶IP和參數(shù)匹配,則可以訪問
* hasRole | 如果有參數(shù),參數(shù)表示角色,則其角色可以訪問
* permitAll | 用戶可以任意訪問
* rememberMe | 允許通過remember-me登錄的用戶訪問
* authenticated | 用戶登錄后可訪問
*/
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception
{
httpSecurity
// CRSF禁用,因為不使用session
.csrf().disable()
// 認證失敗處理類
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 過濾請求
.authorizeRequests()
// 對于登錄login 允許匿名訪問
.antMatchers("/login").anonymous()
.antMatchers(
HttpMethod.GET,
"/*.html",
"/**/*.html",
"/**/*.css",
"/file/**",
"/**/*.js"
).permitAll()
// 除上面外的所有請求全部需要鑒權(quán)認證
.anyRequest().authenticated()
.and()
.headers().frameOptions().disable();
httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
// 添加JWT filter
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
/**
* 強散列哈希加密實現(xiàn)
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder()
{
return new BCryptPasswordEncoder();
}
/**
* 身份認證接口
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
{
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
}- @EnableGlobalMethodSecurity :當我們想要開啟spring方法級安全時,只需要在任何 @Configuration實例上使用 @EnableGlobalMethodSecurity 注解就能達到此目的。
- prePostEnabled = true 會解鎖 @PreAuthorize 和 @PostAuthorize 兩個注解。從名字就可以看出@PreAuthorize 注解會在方法執(zhí)行前進行驗證,而 @PostAuthorize 注解會在方法執(zhí)行后進行驗證。
/**
* 自定義權(quán)限實現(xiàn),ss取自SpringSecurity首字母
*/
@Service("ss")
public class PermissionService
{
/** 所有權(quán)限標識 */
private static final String ALL_PERMISSION = "*:*:*";
/** 管理員角色權(quán)限標識 */
private static final String SUPER_ADMIN = "admin";
private static final String ROLE_DELIMETER = ",";
private static final String PERMISSION_DELIMETER = ",";
@Autowired
private TokenService tokenService;
/**
* 驗證用戶是否具備某權(quán)限
*
* @param permission 權(quán)限字符串
* @return 用戶是否具備某權(quán)限
*/
public boolean hasPermi(String permission)
{
if (StringUtils.isEmpty(permission))
{
return false;
}
LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions()))
{
return false;
}
return hasPermissions(loginUser.getPermissions(), permission);
}
/**
* 驗證用戶是否不具備某權(quán)限,與 hasPermi邏輯相反
*
* @param permission 權(quán)限字符串
* @return 用戶是否不具備某權(quán)限
*/
public boolean lacksPermi(String permission)
{
return hasPermi(permission) != true;
}
/**
* 驗證用戶是否具有以下任意一個權(quán)限
*
* @param permissions 以 PERMISSION_NAMES_DELIMETER 為分隔符的權(quán)限列表
* @return 用戶是否具有以下任意一個權(quán)限
*/
public boolean hasAnyPermi(String permissions)
{
if (StringUtils.isEmpty(permissions))
{
return false;
}
LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions()))
{
return false;
}
Set<String> authorities = loginUser.getPermissions();
for (String permission : permissions.split(PERMISSION_DELIMETER))
{
if (permission != null && hasPermissions(authorities, permission))
{
return true;
}
}
return false;
}
/**
* 判斷用戶是否擁有某個角色
*
* @param role 角色字符串
* @return 用戶是否具備某角色
*/
public boolean hasRole(String role)
{
if (StringUtils.isEmpty(role))
{
return false;
}
LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles()))
{
return false;
}
for (SysRole sysRole : loginUser.getUser().getRoles())
{
String roleKey = sysRole.getRoleKey();
if (SUPER_ADMIN.contains(roleKey) || roleKey.contains(StringUtils.trim(role)))
{
return true;
}
}
return false;
}
/**
* 驗證用戶是否不具備某角色,與 isRole邏輯相反。
*
* @param role 角色名稱
* @return 用戶是否不具備某角色
*/
public boolean lacksRole(String role)
{
return hasRole(role) != true;
}
/**
* 驗證用戶是否具有以下任意一個角色
*
* @param roles 以 ROLE_NAMES_DELIMETER 為分隔符的角色列表
* @return 用戶是否具有以下任意一個角色
*/
public boolean hasAnyRoles(String roles)
{
if (StringUtils.isEmpty(roles))
{
return false;
}
LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles()))
{
return false;
}
for (String role : roles.split(ROLE_DELIMETER))
{
if (hasRole(role))
{
return true;
}
}
return false;
}
/**
* 判斷是否包含權(quán)限
*
* @param permissions 權(quán)限列表
* @param permission 權(quán)限字符串
* @return 用戶是否具備某權(quán)限
*/
private boolean hasPermissions(Set<String> permissions, String permission)
{
return permissions.contains(ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));
}
} /**
* 獲取用戶列表
*/
@PreAuthorize("@ss.hasPermi('system:user:list')")
@GetMapping("/list")
public TableDataInfo list(SysUser user)
{
startPage();
List<SysUser> list = userService.selectUserList(user);
return getDataTable(list);
}
- antMatchers():可傳入多個String參數(shù),用于匹配多個請求API。結(jié)合permitAll(),可同時對多個請求設(shè)置忽略認證。
- and():用于表示上一項配置已經(jīng)結(jié)束,下一項配置將開始。
創(chuàng)建一個Service文件用于Security查詢用戶信息:
@Service
public class UserDetailsServiceImpl implements UserDetailsService
{
private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);
@Autowired
private ISysUserService userService;
@Autowired
private SysPermissionService permissionService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
// 根據(jù)賬戶號查詢用戶信息
SysUser user = userService.selectUserByUserName(username);
if (StringUtils.isNull(user))
{
log.info("登錄用戶:{} 不存在.", username);
throw new UsernameNotFoundException("登錄用戶:" + username + " 不存在");
}
else if (UserStatus.DELETED.getCode().equals(user.getDelFlag()))
{
log.info("登錄用戶:{} 已被刪除.", username);
throw new BaseException("對不起,您的賬號:" + username + " 已被刪除");
}
else if (UserStatus.DISABLE.getCode().equals(user.getStatus()))
{
log.info("登錄用戶:{} 已被停用.", username);
throw new BaseException("對不起,您的賬號:" + username + " 已停用");
}
return createLoginUser(user);
}
public UserDetails createLoginUser(SysUser user)
{
// 獲取用戶權(quán)限
return new LoginUser(user, permissionService.getMenuPermission(user));
}
}
當用戶請求時,Security便會攔截請求,取出其中的username字段,從Service中查詢該賬戶,并將信息填充到一個userDetails對象中返回。這樣Security便拿到了填充后的userDetails對象,也就是獲取到了包括權(quán)限在內(nèi)的用戶信息。
過濾規(guī)則
WebSecurityConfigurerAdapter.configure(HttpSecurity http)
授權(quán)方式
Security的授權(quán)方式有兩種:
- WEB授權(quán)(基于請求URL),在configure(HttpSecurity httpSecurity)中設(shè)置。
- 方法授權(quán)(在方法上使用注解授權(quán))
WEB授權(quán)
@Override
httpSecurity.authorizeRequests() // 對請求進行授權(quán)
.antMatchers("/test1").hasAuthority("p1") // 設(shè)置/test1接口需要p1權(quán)限
.antMatchers("/test2").hasAnyRole("admin", "manager") // 設(shè)置/test2接口需要admin或manager角色
.and()
.headers().frameOptions().disable();
其中:
- hasAuthority(“p1”)表示需要p1權(quán)限
- hasAnyAuthority(“p1”, “p2”)表示p1或p2權(quán)限皆可
同理:
- hasRole(“p1”)表示需要p1角色
- hasAnyRole(“p1”, “p2”)表示p1或p2角色皆可
方法授權(quán)
/**
* 獲取用戶列表
*/
@PreAuthorize("@ss.hasPermi('system:user:list')")
@GetMapping("/list")
public TableDataInfo list(SysUser user)
{
startPage();
List<SysUser> list = userService.selectUserList(user);
return getDataTable(list);
}
順序優(yōu)先級
校驗是按照配置的順序從上往下進行。若某個條件已匹配過,則后續(xù)同條件的匹配將不再被執(zhí)行。也就是說,若多條規(guī)則是相互矛盾的,則只有第一條規(guī)則生效。典型地:
// 只有具有權(quán)限a的用戶才能訪問/get/resource
httpSecurity.authorizeRequests()
.antMatchers("/get/resource").hasAuthority("a")
.anyRequest().permitAll();
// 所有用戶都能訪問/get/resource
httpSecurity.authorizeRequests()
.anyRequest().permitAll()
.antMatchers("/get/resource").hasAuthority("a");
但若是包含關(guān)系,則需要將細粒度的規(guī)則放在前面:
// /get/resource需要權(quán)限a,除此之外所有的/get/**都可以隨意訪問
httpSecurity.authorizeRequests()
.antMatchers("/get/resource").hasAuthority("a")
.antMatchers("/get/**").permitAll();
登出
httpSecurity.logoutUrl()和httpSecurity.addLogoutHandler()是沖突的,通常只用其中之一。對于使用token且服務(wù)端不進行存儲的情況,不需要請求服務(wù)端進行登出,直接由前端將token丟棄即可。
httpSecurity.logout().logoutUrl(“/logout”).logoutSuccessHandler(logoutSuccessHandler);
/**
* 自定義退出處理類 返回成功
*/
@Configuration
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler
{
@Autowired
private TokenService tokenService;
/**
* 退出處理
*
* @return
*/
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException
{
LoginUser loginUser = tokenService.getLoginUser(request);
if (StringUtils.isNotNull(loginUser))
{
String userName = loginUser.getUsername();
// 刪除用戶緩存記錄
tokenService.delLoginUser(loginUser.getToken());
}
ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(HttpStatus.SUCCESS, "退出成功")));
}
}
跨域
httpSecurity.cors() .and() .csrf().disable();
cors()允許跨域資源訪問。
csrf().disable()禁用跨域安全驗證。
認證失敗處理類
/**
* 認證失敗處理類 返回未授權(quán)
*/
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable
{
private static final long serialVersionUID = -8970718410437077606L;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
throws IOException
{
int code = HttpStatus.UNAUTHORIZED;
String msg = StringUtils.format("請求訪問:{},認證失敗,無法訪問系統(tǒng)資源", request.getRequestURI());
ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));
}
}
到此這篇關(guān)于Spring Security實現(xiàn)基于角色的訪問控制框架的文章就介紹到這了,更多相關(guān)Spring Security訪問控制框架內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java中使用json與前臺Ajax數(shù)據(jù)交互的方法
這篇文章主要為大家詳細介紹了Java中使用json與前臺Ajax數(shù)據(jù)交互的方法,分享Ajax獲取顯示Json數(shù)據(jù)的一種方法,感興趣的小伙伴們可以參考一下2016-06-06
MyBatis-Plus通過插件將數(shù)據(jù)庫表生成Entiry,Mapper.xml,Mapper.class的方式
今天小編就為大家分享一篇關(guān)于MyBatis-Plus通過插件將數(shù)據(jù)庫表生成Entiry,Mapper.xml,Mapper.class的方式,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2019-02-02
Java中的system.getProperty()的作用及使用方法
System.getProperty()?方法用于獲取系統(tǒng)屬性的值,該方法接受一個字符串參數(shù),表示要獲取的系統(tǒng)屬性的名稱,返回值為字符串類型,表示該屬性的值,接下來通過本文給大家介紹Java中的system.getProperty()的作用及使用方法,感興趣的朋友跟隨小編一起看看吧2023-05-05
關(guān)于spring?data?jpa?模糊查詢like的坑點
這篇文章主要介紹了關(guān)于spring?data?jpa?模糊查詢like的坑點,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-12-12
Mybatisplus創(chuàng)建Spring?Boot工程打包錯誤的解決方式
最近在實戰(zhàn)springboot遇到了一些坑,記錄一下,下面這篇文章主要給大家介紹了關(guān)于Mybatisplus創(chuàng)建Spring?Boot工程打包錯誤的解決方式,文中通過圖文介紹的介紹的非常詳細,需要的朋友可以參考下2023-03-03

