Spring Boot 訪問安全之認證和鑒權(quán)詳解
在web應用中有大量場景需要對用戶進行安全校,一般人的做法就是硬編碼的方式直接埋到到業(yè)務代碼中,但可曾想過這樣做法會導致代碼不夠簡潔(大量重復代碼)、有個性化時難維護(每個業(yè)務邏輯訪問控制策略都不相同甚至差異很大)、容易發(fā)生安全泄露(有些業(yè)務可能不需要當前登錄信息,但被訪問的數(shù)據(jù)可能是敏感數(shù)據(jù)由于遺忘而沒有受到保護)。
為了更安全、更方便的進行訪問安全控制,我們可以想到的就是使用springmvc的攔截器(HandlerInterceptor),但其實更推薦使用更為成熟的spring security來完成認證和鑒權(quán)。
攔截器
攔截器HandlerInterceptor確實可以幫我們完成登錄攔截、或是權(quán)限校驗、或是防重復提交等需求。其實基于它也可以實現(xiàn)基于url或方法級的安全控制。
如果你對spring mvc的請求處理流程相對的了解,它的原理容易理解。
public interface HandlerInterceptor {
/**
* Intercept the execution of a handler. Called after HandlerMapping determined
* an appropriate handler object, but before HandlerAdapter invokes the handler.
*
* 在業(yè)務處理器處理請求之前被調(diào)用。預處理,可以進行編碼、安全控制、權(quán)限校驗等處理
*
* handler:controller內(nèi)的方法,可以通過HandlerMethod method= ((HandlerMethod)handler);獲取到@RequestMapping
*/
boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
/**
* Intercept the execution of a handler. Called after HandlerAdapter actually
* invoked the handler, but before the DispatcherServlet renders the view.
*
* 在業(yè)務處理器處理請求執(zhí)行完成后,生成視圖之前執(zhí)行。后處理(調(diào)用了Service并返回ModelAndView,但未進行頁面渲染),有機會修改ModelAndView
*/
void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception;
/**
* Callback after completion of request processing, that is, after rendering
* the view. Will be called on any outcome of handler execution, thus allows
* for proper resource cleanup.
*
* 在DispatcherServlet完全處理完請求后被調(diào)用,可用于清理資源等。返回處理(已經(jīng)渲染了頁面)
*
*/
void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception;
}
//你可以基于有些url進行攔截
@Configuration
public class UserSecurityInterceptor extends WebMvcConfigurerAdapter {
@Override
public void addInterceptors(InterceptorRegistry registry) {
String[] securityUrls = new String[]{"/**"};
String[] excludeUrls = new String[]{"/**/esb/**", "/**/dictionary/**"};
registry.addInterceptor(userLoginInterceptor()).excludePathPatterns(excludeUrls).addPathPatterns(securityUrls);
super.addInterceptors(registry);
}
/** fixed: url中包含// 報錯
* org.springframework.security.web.firewall.RequestRejectedException: The request was rejected because the URL was not normalized.
* @return
*/
@Bean
public HttpFirewall allowUrlEncodedSlashHttpFirewall() {
DefaultHttpFirewall firewall = new DefaultHttpFirewall();
firewall.setAllowUrlEncodedSlash(true);
return firewall;
}
@Bean
public AuthInterceptor userLoginInterceptor() {
return new AuthInterceptor();
}
public class AuthInterceptor implements HandlerInterceptor {
public Logger logger = LoggerFactory.getLogger(AuthInterceptor.class);
@Autowired
private ApplicationContext applicationContext;
public AuthInterceptor() {
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
LoginUserInfo user = null;
try {
user = (LoginUserInfo) SSOUserUtils.getCurrentLoginUser();
} catch (Exception e) {
logger.error("從SSO登錄信息中獲取用戶信息失??! 詳細錯誤信息:%s", e);
throw new ServletException("從SSO登錄信息中獲取用戶信息失??!", e);
}
String[] profiles = applicationContext.getEnvironment().getActiveProfiles();
if (!Arrays.isNullOrEmpty(profiles)) {
if ("dev".equals(profiles[0])) {
return true;
}
}
if (user == null || UserUtils.ANONYMOUS_ROLE_ID.equals(user.getRoleId())) {
throw new ServletException("獲取登錄用戶信息失敗!");
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
}
認證
確認一個訪問請求發(fā)起的時候背后的用戶是誰,他的用戶信息是怎樣的。在spring security 里面認證支持很多種方式,最簡單的就是用戶名密碼,還有LDAP、OpenID、CAS等等。
而在我們的系統(tǒng)里面,用戶信息需要通過kxtx-sso模塊進行獲取。通過sso認證比較簡單,就是要確認用戶是否通過會員系統(tǒng)登錄,并把登錄信息包裝成授權(quán)對象放到SecurityContext中,通過一個filter來完成:
@Data
@EqualsAndHashCode(callSuper = false)
public class SsoAuthentication extends AbstractAuthenticationToken {
private static final long serialVersionUID = -1799455508626725119L;
private LoginUserInfo user;
public SsoAuthentication(LoginUserInfo user) {
super(null);
this.user = user;
}
@Override
public Object getCredentials() {
return "kxsso";
}
@Override
public Object getPrincipal() {
return user;
}
@Override
public String getName() {
return user.getName();
}
}
public class SsoAuthenticationProcessingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
LoginUserInfo user = (LoginUserInfo) SSOUserUtils.getCurrentLoginUser();
SsoAuthentication auth = new SsoAuthentication(user );
SecurityContextHolder.getContext().setAuthentication(auth);
filterChain.doFilter(request, response);
}
}
@Component
public class SsoAuthenticationProvider implements AuthenticationProvider {
@Value("${env}")
String env;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
LoginUserInfo loginUserInfo = (LoginUserInfo) authentication.getPrincipal();
/*
* DEV環(huán)境允許匿名用戶訪問,方便調(diào)試,其他環(huán)境必須登錄。
*/
if (!UserUtils.ANONYMOUS_ROLE_ID.equals(loginUserInfo.getRoleId()) || "dev".equals(env)) {
authentication.setAuthenticated(true);
} else {
throw new BadCredentialsException("請登錄");
}
return authentication;
}
@Override
public boolean supports(Class<?> authentication) {
return SsoAuthentication.class.equals(authentication);
}
}
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
// 關閉session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and();
// 允許訪問所有URL,通過方法保護的形式來限制訪問。
http.authorizeRequests().anyRequest().permitAll();
// 注冊sso filter
http.addFilterBefore(ssoAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class);
}
@Bean
SsoAuthenticationProcessingFilter ssoAuthenticationProcessingFilter() {
return new SsoAuthenticationProcessingFilter();
}
}
鑒權(quán)
控制一個功能是否能被當前用戶訪問,對不符合要求的用戶予以拒絕。spring security 主要有兩種控制點:
- 基于請求路徑的:控制某一URL模式必須符合某種要求;
- 基于方法的:控制某一方法必須符合某種要求;
而控制形式就比較多樣化了:
- 代碼配置;
- xml配置;
- 注解控制;
- el表達式;
- 自定義訪問控制器;
目前鑒權(quán)的需求比較簡單:登錄允許訪問,未登錄禁止訪問。因此可以定義了一個切面,控制所有需要安全控制的Controller。
spring security 提供了一些注解:
| @PreAuthorize |
控制一個方法是否能夠被調(diào)用,業(yè)務方法(HandlerMethod )的前置處理,比如: @PreAuthorize("#id<10")限制只能查詢Id小于10的用戶 @PreAuthorize("principal.username.equals(#username)")限制只能查詢自己的信息 @PreAuthorize("#user.name.equals('abc')")限制只能新增用戶名稱為abc的用戶 |
| @PostAuthorize |
業(yè)務方法調(diào)用完之后進行權(quán)限檢查,后置處理,比如: @PostAuthorize("returnObject.id%2==0") public User find(int id) {} 返回值的id是偶數(shù)則表示校驗通過,否則表示校驗失敗,將拋出AccessDeniedException |
| @PreFilter |
對集合類型的參數(shù)進行過濾,比如: 對集合ids中id不為偶數(shù)的進行移除 @PreFilter(filterTarget="ids", value="filterObject%2==0") public void delete(List<Integer> ids, List<String> usernames) {} |
| @PostFilter |
對集合類型的返回值進行過濾,比如: 將對返回結(jié)果中id不為偶數(shù)的list中的對象進行移除 @PostFilter("filterObject.id%2==0") public List<User> findAll() {} |
| @AuthenticationPrincipal | 解決在業(yè)務方法內(nèi)對當前用戶信息的方法 |
@Aspect
@Component
public class InControllerAspect {
@Autowired
BeforeInControllerMethods beforeInMethods;
@Pointcut("execution(public * com.kxtx.oms.portal.controller.in.*.*(..)) && @annotation(org.springframework.web.bind.annotation.RequestMapping)")
public void hp() {
};
@Before("hp()")
public void befor() {
beforeInMethods.before();
}
}
@Component
public class BeforeInControllerMethods {
//@PreAuthorize("authenticated")要求所有訪問此方法的用戶必須登錄
@PreAuthorize("authenticated")
public void before() {
}
}
//用戶信息獲取
@RequestMapping("/order/submit")
public ModelAndView findMessagesForUser(@AuthenticationPrincipal CustomUser customUser) {
// .. find messages for this user and return them ...
}
是不是有點復雜,復雜的是表現(xiàn)形式,實際上需要真正理解它的目的(為了要解決什么問題)。
參考資料
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
Springboot JPA打印SQL語句及參數(shù)的實現(xiàn)
在SpringBoot項目中調(diào)試和優(yōu)化數(shù)據(jù)庫操作是很常見的需求,本文主要介紹了Springboot JPA打印SQL語句及參數(shù)的實現(xiàn),具有一定的參考價值,感興趣的可以了解一下2024-06-06
spring boot整合flyway實現(xiàn)數(shù)據(jù)的動態(tài)維護的示例代碼
本文主要介紹了spring boot整合flyway實現(xiàn)數(shù)據(jù)的動態(tài)維護的示例代碼,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2025-04-04
dubbo將異常轉(zhuǎn)換成RuntimeException的原因分析?ExceptionFilter
這篇文章主要介紹了dubbo將異常轉(zhuǎn)換成RuntimeException的原因分析?ExceptionFilter問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-03-03
SpringBoot 將多個Excel打包下載的實現(xiàn)示例
本文主要介紹了SpringBoot 將多個Excel打包下載的實現(xiàn)示例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2024-12-12
Liquibase 在 Spring Boot 中的使用詳細介紹
Liquibase 提供了靈活的變更集機制,支持創(chuàng)建表、修改列、填充數(shù)據(jù)、回滾變更等多種操作,本文將通過多個豐富的示例,詳細講解如何在 Spring Boot項目中使用 Liquibase,感興趣的朋友跟隨小編一起看看吧2024-12-12
Java thread.isInterrupted() 返回值不確定結(jié)果分析解決
這篇文章主要介紹了Java thread.isInterrupted() 返回值不確定結(jié)果分析,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習吧2022-12-12

