Spring Security
一. 簡介
Spring Security是一個能夠為基於Spring的企業應用系統提供聲明式的安全訪問控制解決方案的安全框架。它提供了一組可以在Spring應用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反轉Inversion of Control ,DI:Dependency Injection 依賴注入)和AOP(面向切面編程)功能,為應用系統提供聲明式的安全訪問控制功能,減少了為企業系統安全控制編寫大量重復代碼的工作。
什么是ACL和RBAC
ACL: Access Control List 訪問控制列表
以前盛行的一種權限設計,它的核心在於用戶直接和權限掛鈎
優點:簡單易用,開發便捷
缺點:用戶和權限直接掛鈎,導致在授予時的復雜性,比較分散,不便於管理
例子:常見的文件系統權限設計, 直接給用戶加權限
RBAC: Role Based Access Control
基於角色的訪問控制系統。權限與角色相關聯,用戶通過成為適當角色的成員而得到這些角色的權限
優點:簡化了用戶與權限的管理,通過對用戶進行分類,使得角色與權限關聯起來
缺點:開發對比ACL相對復雜
例子:基於RBAC模型的權限驗證框架與應用 Apache Shiro、spring Security
BAT企業 ACL,一般是對報表系統,阿里的ODPS
二. 入門案例
2.1 添加依賴
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
</parent>
<dependencies>
<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>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.17</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.social</groupId>
<artifactId>spring-social-web</artifactId>
<version>1.1.6.RELEASE</version>
</dependency>
</dependencies>
2.2 請求
我們任意編寫一個接口,然后進行訪問,會直接跳轉到一個登錄頁面
三. 自定義用戶登錄處理
3.1 安全配置
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() //采用表單登錄
.and()
.authorizeRequests() //請求認證
.anyRequest() //對於任何請求都需要認證
.authenticated(); //認證通過了才能訪問
}
}
3.2 自定義用戶認證
@Component
public class UserAuthentication implements UserDetailsService {
@Resource
private SysUserRepository sysUserRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = sysUserRepository.findByNickyName(username);
if (null == sysUser) {
return new User(username, null, null);
}else {
return new User(username, sysUser.getPassword(),
AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
}
在實際的應用過程中,當我們發起請求的時候,springSecurity處理用戶登錄的過濾器是UsernamePasswordAuthenticationFilter這個過濾器,而這個過濾器會將用戶提交的用戶名和密碼交由UserDetailsService的實現類來處理。具體的處理流程如下圖所示:
3.3 密碼加密校驗
密碼的加密校驗需要實現PasswordEncoder這個接口,接口中有兩個方法,
@Component
public class CustomizePasswordEncoder implements PasswordEncoder {
// 注冊的時候使用, 人為的去調用
@Override
public String encode(CharSequence rawPassword) {
return null;
}
// 當在返回UserDetails,會自動的去實現校驗
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return false;
}
}
實際工作中我們可以直接使用spring security中默認的密碼處理方式就完全可以滿足日常的開發。
3.4 自定義登錄頁面
spring security中定義的登錄頁面有可能不滿足需求,需要自己來實現一個登錄頁面,處理的方式為只需要在3.1節方法中 formLogin() 方法的后面加上loginPage()方法即可,如下代碼所示:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() //采用表單登錄
.loginPage("/login.html")
.and()
.authorizeRequests() //請求認證
.anyRequest() //對於任何請求都需要認真
.authenticated(); //認證通過了才能訪問
}
這樣配置會發現報如下的錯誤:
這個錯誤是很多的初學者容易犯的一個錯誤,原因是因為對於任何的頁面都需要認證,所以就在這里無限循環下去了。我們需要接着調整代碼,如下所示:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() //采用表單登錄
.loginPage("/login.html")
.loginProcessingUrl("/authentication/form") //登錄頁面提交的地址
.and()
.authorizeRequests() //請求認證
.antMatchers("/login.html").permitAll() //如果是登錄頁面直接讓其訪問
.anyRequest() //對於任何請求都需要認真
.authenticated(); //認證通過了才能訪問
}
3.5 編寫自己的登錄頁面
<form action="/authentication/form" method="post">
Username: <input name="username"> <br>
Password: <input name="password" type="password"> <br>
<button>提交</button>
</form>
當我們實現了自己的登錄頁面后發現還是無法登錄,原因在於我們沒有加上csrf(跨站請求偽造),我們暫時先將其禁用,代碼如下:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() //采用表單登錄
.loginPage("/login.html")
.loginProcessingUrl("/authentication/form") //登錄頁面提交的地址
.and()
.authorizeRequests() //請求認證
.antMatchers("/login.html").permitAll() //如果是登錄頁面直接讓其訪問
.anyRequest() //對於任何請求都需要認真
.authenticated() //認證通過了才能訪問
.and()
.csrf().disable(); //關閉跨站請求偽造功能
}
四. 登錄成功與失敗處理
4.1 登錄成功處理
在spring security中,當我們登錄成功后默認是跳轉到用戶登錄之前的請求,這個在當今SPA(Single Page Application)應用流行的今天,肯定是不適用的,我們需要的是異步的請求,返回登錄成功的信息。
要實現用戶登錄成功處理,需要實現AuthenticationSuccessHandler這個接口,然后實現接口中的方法:
@Component
public class CustomizeAuthenticationSuccessHanler
implements AuthenticationSuccessHandler {
private Logger logger = LoggerFactory
.getLogger(CustomizeAuthenticationSuccessHanler.class);
//該bean是springmvc啟動的時候實例化的一個對象,納入到容器中
@Autowired
private ObjectMapper objectMapper;
/**
* authentication中包含了用戶的各種信息,包括UserDetail信息
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication)
throws IOException, ServletException {
logger.info("登錄成功");
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(objectMapper.writeValueAsString(authentication));
}
}
4.2 登錄失敗處理
通過上面的演示我們能看到每次登錄,還是回到登錄頁面,在異步請求下這種是無法滿足我們的需求的,所以需要自定義登錄失敗處理。要實現AuthenticationFailureHandler這個接口,如下所示:
@Component
public class CustomerAuthenticationFailHandler
implements AuthenticationFailureHandler {
private Logger logger = LoggerFactory
.getLogger(CustomerAuthenticationFailHandler.class);
@Autowired
private ObjectMapper objectMapper;
@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception)
throws IOException, ServletException {
logger.info("登錄失敗");
Map<String, Object> map = new HashMap<>();
map.put("code", -1);
map.put("msg", "用戶名或密碼錯誤");
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(objectMapper.writeValueAsString(map));
}
}
安全配置代碼如下:
五. 記住我
5.1 基本原理
在前端頁面的請求參數必須叫remember-me
5.2 功能實現
@Autowired
private UserDetailsService userAuthentication;
@Autowired
private DataSource dataSource;
// 該Bean的主要作用是,可以用於創建數據表並且,當用戶直接訪問的時候直接從
// 數據庫查詢用戶信息
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository
= new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
5.3 安全配置
六. 圖片驗證碼
在實際的應用過程中,為了防止用戶的惡意請求,我們通常都會設置圖片驗證碼功能,而springsecurity並沒有提供現有的實現,需要開發人員自行的實現。
6.1 封裝驗證碼類
public class ImageCode {
private BufferedImage bufferedImage;
// code是隨機字母,需要存儲在session中
private String code;
// 過期時間
private LocalDateTime expireTime;
// 第三個參數為過期的時間
public ImageCode(BufferedImage bufferedImage, String code, int seconds) {
this.bufferedImage = bufferedImage;
this.code = code;
this.expireTime = LocalDateTime.now().plusSeconds(seconds); //設置過期的時間點
}
// 驗證碼是否過期
public boolean isExpire() {
return LocalDateTime.now().isAfter(expireTime); //當前時間是否在過期時間之后
}
// setters、getters、other constructors
}
6.2 請求控制類
@RestController
public class ImageCodeController {
//操作Session的工具類
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
//session中存放驗證碼的key
public static final String IMAGE_CODE_SESSION_KEY = "IMAGE_CODE_SESSION_KEY";
@RequestMapping("/image/code")
public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
//生成圖片驗證碼,具體的實現照搬現有的工具類
ImageCode imageCode = generate();
//將圖片驗證碼存放到session中,
sessionStrategy.setAttribute(new ServletWebRequest(request), IMAGE_CODE_SESSION_KEY, imageCode);
//將圖片寫回到頁面
ImageIO.write(imageCode.getBufferedImage(), "JPEG", response.getOutputStream());
}
}
6.3 過濾器的編寫
public class ValidataCodeFilter extends OncePerRequestFilter {
// 所有登錄失敗都交由該類來處理
private CustomerAuthenticationFailHandler customerAuthenticationFailHandler;
public void setCustomerAuthenticationFailHandler(CustomerAuthenticationFailHandler customerAuthenticationFailHandler) {
this.customerAuthenticationFailHandler = customerAuthenticationFailHandler;
}
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
//判斷是否為登錄,並且請求方式為post
if(StringUtils.equals("/authentication/form", request.getRequestURI())
&& StringUtils.equals(request.getMethod(), "POST")) {
try{
validate(new ServletWebRequest(request)); //校驗驗證碼
}catch (AuthenticationException exception) {
customerAuthenticationFailHandler.onAuthenticationFailure(request, response, exception);
return;
}
}
filterChain.doFilter(request, response);
}
// 具體的校驗邏輯
public void validate(ServletWebRequest request) throws ServletRequestBindingException {
// 從session中獲取驗證碼信息
ImageCode imageCode = (ImageCode) sessionStrategy.getAttribute(request,
ImageCodeController.IMAGE_CODE_SESSION_KEY);
// 獲取請求的參數
String validateCode = ServletRequestUtils.getStringParameter(request.getRequest(), "codeImage");
if(StringUtils.isEmpty(validateCode)) {
throw new ValidateException("驗證碼不能為空");
}
if(imageCode == null) {
throw new ValidateException("驗證碼不存在");
}
if(imageCode.isExpire()) {
throw new ValidateException("驗證碼過期");
sessionStrategy.removeAttribute(request,
ImageCodeController.IMAGE_CODE_SESSION_KEY);
}
if(!validateCode.equals(imageCode.getCode())) {
throw new ValidateException("驗證碼不正確");
}
sessionStrategy.removeAttribute(request,
ImageCodeController.IMAGE_CODE_SESSION_KEY);
}
}
6.4 登錄異常處理
springSecurity中處理用戶登錄異常都應該由AuthenticationException這個異常來處理,所以我們需要自定義驗證碼校驗失敗的異常類:
public class ValidateException extends AuthenticationException {
public ValidateException(String msg) {
super(msg);
}
}
七. 手機號登錄
手機號登錄與用戶名密碼登錄邏輯相同,所以我們在使用手機號登錄系統的時候可以完全拷貝用戶名密碼登錄的邏輯,那么前提是我們必須得搞懂用戶名密碼登錄的邏輯。
7.1 編寫Token
編寫手機號認證Token, 模仿UsernamePasswordAuthenticationToken這個類來實現。
/**
* 短信驗證碼Token, 用於封裝用戶使用手機登錄的相關信息。
*/
public class SmsAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
// principal在未登錄之前封裝用戶的手機號,登錄之后封裝用戶的信息
private final Object principal;
// ~ Constructors
/**
* This constructor can be safely used by any code that wishes to create a
* <code>UsernamePasswordAuthenticationToken</code>, as the {@link #isAuthenticated()}
* will return <code>false</code>.
*/
public SmsAuthenticationToken(Object principal) {
super(null);
this.principal = principal;
setAuthenticated(false);
}
/**
* This constructor should only be used by <code>AuthenticationManager</code> or
* <code>AuthenticationProvider</code> implementations that are satisfied with
* producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
* authentication token.
*
* @param principal
* @param authorities
*/
public SmsAuthenticationToken(Object principal,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true); // must use super, as we override
}
@Override
public Object getCredentials() {
return null;
}
public Object getPrincipal() {
return this.principal;
}
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException(
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
}
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
}
}
7.2 編寫Filter
手機號的過濾器可以模仿 UsernamePasswordAuthenticationFilter 來實現。
public class SmsAuthenticationFilter extends
AbstractAuthenticationProcessingFilter {
// ~ Static fields/initializers
public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";
private String mobileParameter = SPRING_SECURITY_FORM_MOBILE_KEY;
private boolean postOnly = true;
public SmsAuthenticationFilter() {
super(new AntPathRequestMatcher("/authentication/mobile", "POST"));
}
// ~ Methods
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
String mobile = obtainMobile(request);
if (mobile == null) {
mobile = "";
}
mobile = mobile.trim();
SmsAuthenticationToken authRequest = new SmsAuthenticationToken(mobile);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
protected String obtainMobile(HttpServletRequest request) {
return request.getParameter(mobileParameter);
}
/**
* Provided so that subclasses may configure what is put into the authentication
* request's details property.
*
* @param request that an authentication request is being created for
* @param authRequest the authenticatio request object that should have its details
*/
protected void setDetails(HttpServletRequest request,
SmsAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
/**
* Sets the parameter name which will be used to obtain the username from the login
* request.
*/
public void setMobileParameter(String mobileParameter) {
Assert.hasText(mobileParameter, "Mobile parameter must not be empty or null");
this.mobileParameter = mobileParameter;
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
}
7.3 Provider
Provider的作用是用來處理對應的Token,校驗用戶名密碼使用的Provider為DaoAuthenticationProvider, 在實現我們自己的Provider的時候,我們去實現AuthenticationProvider。
public class SmsCredentialsProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
public UserDetailsService getUserDetailsService() {
return userDetailsService;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//實際傳入過來的就是 SmsAuthenticationToken, 因為supports方法已經進行了判斷,如果為true,才進入該方法
SmsAuthenticationToken smsAuthenticationToken = (SmsAuthenticationToken)authentication;
//使用
UserDetails user = userDetailsService.loadUserByUsername((String)smsAuthenticationToken.getPrincipal());
if(null == user) {
throw new InternalAuthenticationServiceException("無法獲取用戶信息");
}
//將用戶信息以及用戶權限 重新構建一個SmsAuthenticationToken
SmsAuthenticationToken token = new SmsAuthenticationToken(user, user.getAuthorities());
token.setDetails(smsAuthenticationToken.getDetails());
return token;
}
/**
* 判斷當前的方法的參數authentication, 是否為SmsAuthenticationToken這個類型,
* 如果是的化,就調用上面的 authenticate 方法。
* @param authentication
* @return
*/
@Override
public boolean supports(Class<?> authentication) {
return SmsAuthenticationToken.class.isAssignableFrom(authentication);
}
}
7.4 發送短信實現
接口的實現
public interface SmsCodeSender {
//發送手機短信驗證碼的
void send(String code, String mobile);
}
實現類
public class DefaultSmsCodeSender implements SmsCodeSender {
@Override
public void send(String code, String mobile) {
System.out.println("往手機 " + mobile + " 上發送的驗證碼為: " + code);
}
}
7.5 配置Filter以及Provider
@Component
public class SmsCodeAuthenticationConfig
extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private CustomizeAuthenticationSuccessHanler customizeAuthenticationSuccessHanler;
@Autowired
private CustomerAuthenticationFailHandler customerAuthenticationFailHandler;
@Autowired
private UserDetailsService userAuthentication;
@Override
public void configure(HttpSecurity http) throws Exception {
SmsAuthenticationFilter smsAuthenticationFilter = new SmsAuthenticationFilter();
//設置AuthenticationManager, 用於同一管理Filter
smsAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
//設置成功處理器
smsAuthenticationFilter.setAuthenticationSuccessHandler(customizeAuthenticationSuccessHanler);
//設置失敗過濾器
smsAuthenticationFilter.setAuthenticationFailureHandler(customerAuthenticationFailHandler);
//實例化Provider
SmsCredentialsProvider smsCredentialsProvider = new SmsCredentialsProvider();
smsCredentialsProvider.setUserDetailsService(userAuthentication);
http.authenticationProvider(smsCredentialsProvider)
.addFilterAfter(smsAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
7.6 安全配置
短信驗證碼的過濾器和圖片驗證碼的邏輯是相同,故在此不作處理。
7.7 頁面的實現
八. session管理
8.1 session並發控制
session的失效時間默認為30min,可以通過 server.servlet.session.timeout類配置。在很多的業務場景下,我們只允許一台設備登錄到服務端。
安全配置
session失效處理邏輯
/**
* 同時多設備登錄處理
*/
public class MultipleSessionHandler implements SessionInformationExpiredStrategy {
@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent event)
throws IOException, ServletException {
HttpServletResponse response = event.getResponse();
response.setContentType("text/plain;charset=utf-8");
response.getWriter().write("其他設備登錄");
}
}
8.2 session集群管理
當我們在集群環境下,用戶每次的請求我們並不能保證每次都是到達同一台服務器,可能會導致session存在於不同的服務器上,而讓用戶重新進行登錄,所以必須要采用一個中間件來存儲用戶的session信息,企業中使用最多的就是redis.
依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-core</artifactId>
<version>2.1.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<version>2.1.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.7.0</version>
</dependency>
applicatoin.yml配置
spring:
redis:
port: 6379
host: localhost
password:
lettuce:
pool:
min-idle: 2
max-active: 8
session:
store-type: redis
九. 退出登錄
.logout() //
.logoutSuccessUrl("/login.html") //退出后跳轉的頁面
.and()
十. 權限管理
權限是大部分的后台管理系統都需要實現的功能,用戶控制不同的角色能夠進行的不同的操作。Spring Security的可以進行用戶的角色權限控制,也可以進行用戶的操作權限控制。在之前的代碼實現上,我們僅僅只是實現用戶的登錄,在用戶信息驗證的時候使用UserDetailsService,但是卻一直忽略了用戶的權限。
10.1 啟動類配置
/**
* 開啟方法的注解安全校驗。
* securedEnabled @Secured("ROLE_abc") 該注解是Spring security提供的
* jsr250Enabled @RolesAllowed("admin") 該注解是 JSR250 支持的注解形式
* prePostEnabled
*/
@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true, jsr250Enabled = true, prePostEnabled = true)
public class SecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SecurityApplication.class, args);
}
}
10.2 基於角色的權限控制
用戶權限的查詢
@Component
public class UserSecurityService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
/**
* 調用形式有兩種:
* 1. 此時構建的 SimpleGrantedAuthority 必須是以 ROLE_ 開頭, 例如 ROLE_admin, ROLE_manager.
* 實現全權限控制的時候使用 @RolesAllowed("ROLE_admin") 或者 @RolesAllowed("admin") 都可以
* 2. 此時構建的 SimpleGrantedAuthority 必須是以 ROLE_ 開頭, 例如 ROLE_admin, ROLE_manager.
* 實現全權限控制的時候使用 @Secured("ROLE_admin") ROLE_是不能省略的。
*
* 其中1,2也只能實現對角色的控制,那么如果細粒度到具體的方法進行控制,需要使用到其他的方式。
* A. new SimpleGrantedAuthority("user:delete") @PreAuthorize("hasAnyAuthority('user:add', 'user:list')") 無法訪問。
* B. new SimpleGrantedAuthority("user:add") @PreAuthorize("hasAnyAuthority('user:add', 'user:list')") 可以訪問。
* C. Arrays.asList(new SimpleGrantedAuthority("user:add"), new SimpleGrantedAuthority("user:list"))
* @PreAuthorize("hasAuthority('user:add') and hasAuthority('user:list')") 可以訪問
* D. new SimpleGrantedAuthority("ROLE_admin") 定義角色
* @PreAuthorize("hasRole('admin')") 可以訪問
*
*
*/
return new User(username, sysUser.getPassword(),
Arrays.asList(new SimpleGrantedAuthority("ROLE_admin")));
}
}
我們在構建SimpleGrantedAuthority對象的時候,用戶的角色必須是以 ROLE_ 開頭,例如 ROLE_admin、ROLE_manager
控制器角色控制
在控制器上進行用戶訪問控制的時候,基於角色有兩種書寫方式:
方式一:@RolesAllowed
/**
* @RolesAllowed 中的值可以寫成 "admin", 例如 @RolesAllowed("admin")
* @RolesAllowed 中的值還可以寫成 "ROLE_admin",例如 @RolesAllowed("ROLE_admin")
*/
@RequestMapping
@RolesAllowed("admin")
public Object getAll() {
return Arrays.asList(new User(10, "張"), new User(20, "李四"));
}
方式二:
/**
* @Secured 中的值必須為 "ROLE_admin",例如 @Secured("ROLE_admin"),ROLE_不能省略
*/
@RequestMapping
@Secured("ROLE_admin")
public Object getAll() {
return Arrays.asList(new User(10, "張"), new User(20, "李四"));
}
10.3 基於操作的權限控制
當然我們也可以使用基於操作的權限控制,這個功能稍顯得有點累贅,因為在實際的項目開發過程中我們都是基於角色的權限控制。
用戶權限查詢
@Component
public class UserSecurityService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
/**
return new User(username, sysUser.getPassword(),
Arrays.asList(new SimpleGrantedAuthority("ROLE_admin")));
*/
return new User(username, sysUser.getPassword(),
Arrays.asList(new SimpleGrantedAuthority("user:list"),
new SimpleGrantedAuthority("user:add")
));
}
}
控制器訪問控制(針對角色)
/**
* @PreAuthorize 中的值可以為 "ROLE_admin", "admin",
* 例如 @PreAuthorize("hasRole('admin')") 或者為
* @PreAuthorize("hasRole('ROLE_admin')")
*/
@RequestMapping
@PreAuthorize("hasRole('admin')")
public Object getAll() {
return Arrays.asList(new User(10, "張"), new User(20, "李四"));
}
控制器訪問控制(針對操作)
@RequestMapping
// @PreAuthorize("hasAuthority('user:add') and hasAuthority('user:list')")
// @PreAuthorize("hasAuthority('user:add') or hasAuthority('user:list')")
@PreAuthorize("hasAnyAuthority('user:add', 'user:list')")
public Object getAll() {
return Arrays.asList(new User(10, "張"), new User(20, "李四"));
}
10.4 訪問無權限處理
.and()
.exceptionHandling()
.accessDeniedHandler(customizeAccessDeniedHandler) //無權限訪問處理
.and()