Spring Boot 訪問安全之認(rèn)證和鑒權(quán)詳解
在web應(yīng)用中有大量場(chǎng)景需要對(duì)用戶進(jìn)行安全校,一般人的做法就是硬編碼的方式直接埋到到業(yè)務(wù)代碼中,但可曾想過這樣做法會(huì)導(dǎo)致代碼不夠簡(jiǎn)潔(大量重復(fù)代碼)、有個(gè)性化時(shí)難維護(hù)(每個(gè)業(yè)務(wù)邏輯訪問控制策略都不相同甚至差異很大)、容易發(fā)生安全泄露(有些業(yè)務(wù)可能不需要當(dāng)前登錄信息,但被訪問的數(shù)據(jù)可能是敏感數(shù)據(jù)由于遺忘而沒有受到保護(hù))。
為了更安全、更方便的進(jìn)行訪問安全控制,我們可以想到的就是使用springmvc的攔截器(HandlerInterceptor),但其實(shí)更推薦使用更為成熟的spring security來完成認(rèn)證和鑒權(quán)。
攔截器
攔截器HandlerInterceptor確實(shí)可以幫我們完成登錄攔截、或是權(quán)限校驗(yàn)、或是防重復(fù)提交等需求。其實(shí)基于它也可以實(shí)現(xiàn)基于url或方法級(jí)的安全控制。
如果你對(duì)spring mvc的請(qǐng)求處理流程相對(duì)的了解,它的原理容易理解。
public interface HandlerInterceptor { /** * Intercept the execution of a handler. Called after HandlerMapping determined * an appropriate handler object, but before HandlerAdapter invokes the handler. * * 在業(yè)務(wù)處理器處理請(qǐng)求之前被調(diào)用。預(yù)處理,可以進(jìn)行編碼、安全控制、權(quán)限校驗(yà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è)務(wù)處理器處理請(qǐng)求執(zhí)行完成后,生成視圖之前執(zhí)行。后處理(調(diào)用了Service并返回ModelAndView,但未進(jìn)行頁(yè)面渲染),有機(jī)會(huì)修改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完全處理完請(qǐng)求后被調(diào)用,可用于清理資源等。返回處理(已經(jīng)渲染了頁(yè)面) * */ void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception; } //你可以基于有些url進(jìn)行攔截 @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中包含// 報(bào)錯(cuò) * 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登錄信息中獲取用戶信息失??! 詳細(xì)錯(cuò)誤信息:%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 { } } }
認(rèn)證
確認(rèn)一個(gè)訪問請(qǐng)求發(fā)起的時(shí)候背后的用戶是誰(shuí),他的用戶信息是怎樣的。在spring security 里面認(rèn)證支持很多種方式,最簡(jiǎn)單的就是用戶名密碼,還有LDAP、OpenID、CAS等等。
而在我們的系統(tǒng)里面,用戶信息需要通過kxtx-sso模塊進(jìn)行獲取。通過sso認(rèn)證比較簡(jiǎn)單,就是要確認(rèn)用戶是否通過會(huì)員系統(tǒng)登錄,并把登錄信息包裝成授權(quán)對(duì)象放到SecurityContext中,通過一個(gè)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("請(qǐng)登錄"); } 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 { // 關(guān)閉session http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and(); // 允許訪問所有URL,通過方法保護(hù)的形式來限制訪問。 http.authorizeRequests().anyRequest().permitAll(); // 注冊(cè)sso filter http.addFilterBefore(ssoAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class); } @Bean SsoAuthenticationProcessingFilter ssoAuthenticationProcessingFilter() { return new SsoAuthenticationProcessingFilter(); } }
鑒權(quán)
控制一個(gè)功能是否能被當(dāng)前用戶訪問,對(duì)不符合要求的用戶予以拒絕。spring security 主要有兩種控制點(diǎn):
- 基于請(qǐng)求路徑的:控制某一URL模式必須符合某種要求;
- 基于方法的:控制某一方法必須符合某種要求;
而控制形式就比較多樣化了:
- 代碼配置;
- xml配置;
- 注解控制;
- el表達(dá)式;
- 自定義訪問控制器;
目前鑒權(quán)的需求比較簡(jiǎn)單:登錄允許訪問,未登錄禁止訪問。因此可以定義了一個(gè)切面,控制所有需要安全控制的Controller。
spring security 提供了一些注解:
@PreAuthorize |
控制一個(gè)方法是否能夠被調(diào)用,業(yè)務(wù)方法(HandlerMethod )的前置處理,比如: @PreAuthorize("#id<10")限制只能查詢Id小于10的用戶 @PreAuthorize("principal.username.equals(#username)")限制只能查詢自己的信息 @PreAuthorize("#user.name.equals('abc')")限制只能新增用戶名稱為abc的用戶 |
@PostAuthorize |
業(yè)務(wù)方法調(diào)用完之后進(jìn)行權(quán)限檢查,后置處理,比如: @PostAuthorize("returnObject.id%2==0") public User find(int id) {} 返回值的id是偶數(shù)則表示校驗(yàn)通過,否則表示校驗(yàn)失敗,將拋出AccessDeniedException |
@PreFilter |
對(duì)集合類型的參數(shù)進(jìn)行過濾,比如: 對(duì)集合ids中id不為偶數(shù)的進(jìn)行移除 @PreFilter(filterTarget="ids", value="filterObject%2==0") public void delete(List<Integer> ids, List<String> usernames) {} |
@PostFilter |
對(duì)集合類型的返回值進(jìn)行過濾,比如: 將對(duì)返回結(jié)果中id不為偶數(shù)的list中的對(duì)象進(jìn)行移除 @PostFilter("filterObject.id%2==0") public List<User> findAll() {} |
@AuthenticationPrincipal | 解決在業(yè)務(wù)方法內(nèi)對(duì)當(dāng)前用戶信息的方法 |
@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 ... }
是不是有點(diǎn)復(fù)雜,復(fù)雜的是表現(xiàn)形式,實(shí)際上需要真正理解它的目的(為了要解決什么問題)。
參考資料
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
淺談SpringBoot集成Redis實(shí)現(xiàn)緩存處理(Spring AOP實(shí)現(xiàn))
這篇文章主要介紹了淺談SpringBoot集成Redis實(shí)現(xiàn)緩存處理(Spring AOP實(shí)現(xiàn)),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-12-12Mybatis結(jié)果集映射一對(duì)多簡(jiǎn)單入門教程
本文給大家介紹Mybatis結(jié)果集映射一對(duì)多簡(jiǎn)單入門教程,包括搭建數(shù)據(jù)庫(kù)環(huán)境的過程,idea搭建maven項(xiàng)目的代碼詳解,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友參考下吧2021-06-06SpringBoot超詳細(xì)講解多數(shù)據(jù)源集成
今天分享下SpringBoot多數(shù)據(jù)源集成,我怕麻煩,這里我覺得我的集成也應(yīng)該是最簡(jiǎn)單的,清晰明了2022-05-05如何優(yōu)雅的拋出Spring Boot注解的異常詳解
這篇文章主要給大家介紹了關(guān)于如何優(yōu)雅的拋出Spring Boot注解的異常的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-12-12SpringBoot2實(shí)現(xiàn)MessageQueue消息隊(duì)列
本文主要介紹了 SpringBoot2實(shí)現(xiàn)MessageQueue消息隊(duì)列,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-04-04在SpringBoot項(xiàng)目中的使用Swagger的方法示例
這篇文章主要介紹了在SpringBoot項(xiàng)目中的使用Swagger的方法示例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-05-05Java 實(shí)現(xiàn)瀏覽器下載文件及文件預(yù)覽
這篇文章主要介紹了Java 實(shí)現(xiàn)瀏覽器下載文件及文件預(yù)覽,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-06-06解析Java編程中對(duì)于包結(jié)構(gòu)的命名和訪問
這篇文章主要介紹了Java編程中對(duì)于包結(jié)構(gòu)的命名和訪問,是Java入門學(xué)習(xí)中的基礎(chǔ)知識(shí),需要的朋友可以參考下2015-12-12