<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
Spring Security是一個能夠為基於Spring的企業應用系統提供宣告式的安全存取控制解決方案的安全框架。它提供了一組可以在Spring應用上下文中設定的Bean,充分利用了Spring IoC(控制反轉),DI(依賴注入)和AOP(面向切面程式設計)功能,為應用系統提供宣告式的安全存取控制功能,減少了為企業系統安全控制編寫大量重複程式碼的工作。
除系統內維護的使用者名稱和密碼認證技術外,Spring Security還支援HTTP層面的認證,包括HTTP基本認證和HTTP摘要認證
HTTP基本認證是在RFC2616中定義的一種認證模式。
有上面可以看出只需要驗證Authentication即可,因此如果不使用瀏覽器存取HTTP基本認證保護的頁面,則自行在請求頭中設定Authorization
也是可以.
HTTP基本認證是一種無狀態的認證方式,與表單認證相比,HTTP基本認證是一種基於HTTP層面的認證方式,無法攜帶session,即無法實現Remember-ME功能。另外,使用者名稱和密碼在傳遞時僅做一次簡單的Base64
編碼,幾乎等同於明文傳輸,極易出現密碼被竊聽和重放攻擊等安全性問題,在實際系統開發中很少使用這種方式來進行安全驗證。 如果有必要,也應使用加密的傳輸層HTTPS
來保障安全.
pom.xml:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
@EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().anyRequest().authenticated().and().httpBasic(); } }
上面的設定最後新增了httpBasic()
,使用http基本認證
存取本地專案,http://localhost:8080
會彈出登陸框,我們看到偵錯工具中返回了401無許可權。
我們使用Spring Security提供的預設的使用者名稱和密碼登陸。
登陸成功後,header中就會有Authorization: Basic dXNlcjo0NWU2NzViOC1hZGYwLTQzNzMtYjA2MS02MGE0YzkzZjA2ZGU=
上面我們實現了HTTP基本認證,我們看看其中Spring Security中是如何做到的?
我們使用HTTP基本認證的時候,在設定類中使用httpBasic()
進行處理。
httpBasic方法:
public HttpBasicConfigurer<HttpSecurity> httpBasic() throws Exception { return (HttpBasicConfigurer)this.getOrApply(new HttpBasicConfigurer()); }
上面可以看出,Spring Security
進行HTTP基本認證是使用HttpBasicConfigurer
設定類進行的。HttpBasicConfigurer.class
:
//構建HttpBasicConfigurer public HttpBasicConfigurer() { this.realmName("Realm"); LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> entryPoints = new LinkedHashMap(); entryPoints.put(X_REQUESTED_WITH, new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)); DelegatingAuthenticationEntryPoint defaultEntryPoint = new DelegatingAuthenticationEntryPoint(entryPoints); defaultEntryPoint.setDefaultEntryPoint(this.basicAuthEntryPoint); this.authenticationEntryPoint = defaultEntryPoint; } //進行設定 public void configure(B http) { //進行認證管理 AuthenticationManager authenticationManager = (AuthenticationManager)http.getSharedObject(AuthenticationManager.class); //宣告basic認證攔截器 BasicAuthenticationFilter basicAuthenticationFilter = new BasicAuthenticationFilter(authenticationManager, this.authenticationEntryPoint); if (this.authenticationDetailsSource != null) { basicAuthenticationFilter.setAuthenticationDetailsSource(this.authenticationDetailsSource); } //註冊一個RememberMeServices RememberMeServices rememberMeServices = (RememberMeServices)http.getSharedObject(RememberMeServices.class); if (rememberMeServices != null) { //設定rememberMeServices basicAuthenticationFilter.setRememberMeServices(rememberMeServices); } //申明basicAuthenticationFilter過濾器 basicAuthenticationFilter = (BasicAuthenticationFilter)this.postProcess(basicAuthenticationFilter); http.addFilter(basicAuthenticationFilter); }
上面宣告BasicAuthenticationFilter
並新增到攔截器鏈中BasicAuthenticationFilter.class
:
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { try { //獲取token UsernamePasswordAuthenticationToken authRequest = this.authenticationConverter.convert(request); //authRequest為空直接放行 if (authRequest == null) { this.logger.trace("Did not process authentication request since failed to find username and password in Basic Authorization header"); chain.doFilter(request, response); return; } //獲取使用者名稱 String username = authRequest.getName(); this.logger.trace(LogMessage.format("Found username '%s' in Basic Authorization header", username)); if (this.authenticationIsRequired(username)) { Authentication authResult = this.authenticationManager.authenticate(authRequest); //建立上下文 SecurityContext context = SecurityContextHolder.createEmptyContext(); context.setAuthentication(authResult); //設定響應的上下文 SecurityContextHolder.setContext(context); if (this.logger.isDebugEnabled()) { this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult)); } this.rememberMeServices.loginSuccess(request, response, authResult); this.onSuccessfulAuthentication(request, response, authResult); } } catch (AuthenticationException var8) { SecurityContextHolder.clearContext(); this.logger.debug("Failed to process authentication request", var8); this.rememberMeServices.loginFail(request, response); this.onUnsuccessfulAuthentication(request, response, var8); if (this.ignoreFailure) { chain.doFilter(request, response); } else { this.authenticationEntryPoint.commence(request, response, var8); } return; } chain.doFilter(request, response); }
BasicAuthenticationEntryPoint
返回進行響應的處理
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { //新增響應響應頭 response.addHeader("WWW-Authenticate", "Basic realm="" + this.realmName + """); response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase()); }
HTTP摘要認證和HTTP基本認證一樣,也是在RFC2616中定義的認證模式,RFC2617專門對這兩種認證模式做了規定。與 HTTP 基本認證相比,HTTP 摘要認證使用對通訊雙方都可知的口令進行校驗,且最終的傳輸資料並非明文形式。
摘要認證是一種協定規定的Web伺服器用來同網頁瀏覽器進行認證資訊協商的方法。它在密碼發出前,先對其應用雜湊函數,這相對於HTTP基本認證傳送明文而言,更安全。
從技術上講,摘要認證是使用亂數來阻止進行密碼分析的MD5加密雜湊函數應用。
HTTP摘要認證流程:
HTTP摘要認證中的相關引數:
在Spring Security中沒有像HTTP基礎認證那樣,通過httpBasic()方法進行整合HTTP摘要認證,但是Spring Security提供了像BasicAuthenticationEntryPoint
一樣的DigestAuthenticationEntryPoint
.就是我們需要將DigestAuthenticationEntryPoint
新增到filter過濾器中去處理。
程式碼如下:WebSecurityConfig
類:
@EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private DigestAuthenticationEntryPoint digestAuthenticationEntryPoint; @Autowired private UserDetailsService userDetailsService; @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().anyRequest().authenticated().and() .exceptionHandling() .authenticationEntryPoint(digestAuthenticationEntryPoint) .and().addFilter(digestAuthenticationFilter()); } public DigestAuthenticationFilter digestAuthenticationFilter(){ DigestAuthenticationFilter digestAuthenticationFilter = new DigestAuthenticationFilter(); digestAuthenticationFilter.setUserDetailsService(userDetailsService); digestAuthenticationFilter.setAuthenticationEntryPoint(digestAuthenticationEntryPoint); return digestAuthenticationFilter; } }
申明DigestAuthenticationEntryPoint
Bean:
@Bean public DigestAuthenticationEntryPoint digestAuthenticationEntryPoint(){ DigestAuthenticationEntryPoint digestAuthenticationEntryPoint = new DigestAuthenticationEntryPoint(); digestAuthenticationEntryPoint.setRealmName("realName"); digestAuthenticationEntryPoint.setKey("tony"); return digestAuthenticationEntryPoint; } @Bean public DigestAuthenticationEntryPoint digestAuthenticationEntryPoint(){ DigestAuthenticationEntryPoint digestAuthenticationEntryPoint = new DigestAuthenticationEntryPoint(); digestAuthenticationEntryPoint.setRealmName("realm"); digestAuthenticationEntryPoint.setKey("tony"); return digestAuthenticationEntryPoint; } @Bean public UserDetailsService userDetailsService() { InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); manager.createUser(User.withUsername("tony").password("123456").roles("admin").build()); return manager; } @Bean public PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); }
存取主頁,http://localhost:8080,返回如下頁面:
我們輸入使用者名稱和密碼登陸。
當長時間未登入,隨機字串到期了也登陸不上。
預設的過期時間為300s,我們可以通過設定時間。DigestAuthenticationEntryPoint
中realmName和key是必須要設定的。
相關原始碼:
public void afterPropertiesSet() { Assert.hasLength(this.realmName, "realmName must be specified"); Assert.hasLength(this.key, "key must be specified"); } public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { //計算過期時間 long expiryTime = System.currentTimeMillis() + (long)(this.nonceValiditySeconds * 1000); //計算簽名值 String signatureValue = DigestAuthUtils.md5Hex(expiryTime + ":" + this.key); //隨機字串 String nonceValue = expiryTime + ":" + signatureValue; //隨機字串base64 String nonceValueBase64 = new String(Base64.getEncoder().encode(nonceValue.getBytes())); String authenticateHeader = "Digest realm="" + this.realmName + "", qop="auth", nonce="" + nonceValueBase64 + """; if (authException instanceof NonceExpiredException) { authenticateHeader = authenticateHeader + ", stale="true""; } logger.debug(LogMessage.format("WWW-Authenticate header sent to user agent: %s", authenticateHeader)); response.addHeader("WWW-Authenticate", authenticateHeader); response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase()); }
進行處理的時候使用DigestAuthenticationFilter
進行處理
public void afterPropertiesSet() { //必須設定userDetailsService Assert.notNull(this.userDetailsService, "A UserDetailsService is required"); //必須設定authenticationEntryPoint Assert.notNull(this.authenticationEntryPoint, "A DigestAuthenticationEntryPoint is required"); } private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { String header = request.getHeader("Authorization"); if (header != null && header.startsWith("Digest ")) { logger.debug(LogMessage.format("Digest Authorization header received from user agent: %s", header)); DigestAuthenticationFilter.DigestData digestAuth = new DigestAuthenticationFilter.DigestData(header); try { //驗證並且解密 digestAuth.validateAndDecode(this.authenticationEntryPoint.getKey(), this.authenticationEntryPoint.getRealmName()); } catch (BadCredentialsException var11) { this.fail(request, response, var11); return; } //快取 boolean cacheWasUsed = true; //快取使用者資料 UserDetails user = this.userCache.getUserFromCache(digestAuth.getUsername()); String serverDigestMd5; try { if (user == null) { cacheWasUsed = false; user = this.userDetailsService.loadUserByUsername(digestAuth.getUsername()); if (user == null) { throw new AuthenticationServiceException("AuthenticationDao returned null, which is an interface contract violation"); } this.userCache.putUserInCache(user); } //伺服器md5摘要 serverDigestMd5 = digestAuth.calculateServerDigest(user.getPassword(), request.getMethod()); if (!serverDigestMd5.equals(digestAuth.getResponse()) && cacheWasUsed) { logger.debug("Digest comparison failure; trying to refresh user from DAO in case password had changed"); user = this.userDetailsService.loadUserByUsername(digestAuth.getUsername()); this.userCache.putUserInCache(user); serverDigestMd5 = digestAuth.calculateServerDigest(user.getPassword(), request.getMethod()); } } catch (UsernameNotFoundException var12) { String message = this.messages.getMessage("DigestAuthenticationFilter.usernameNotFound", new Object[]{digestAuth.getUsername()}, "Username {0} not found"); this.fail(request, response, new BadCredentialsException(message)); return; } String message; if (!serverDigestMd5.equals(digestAuth.getResponse())) { logger.debug(LogMessage.format("Expected response: '%s' but received: '%s'; is AuthenticationDao returning clear text passwords?", serverDigestMd5, digestAuth.getResponse())); message = this.messages.getMessage("DigestAuthenticationFilter.incorrectResponse", "Incorrect response"); this.fail(request, response, new BadCredentialsException(message)); } else if (digestAuth.isNonceExpired()) { message = this.messages.getMessage("DigestAuthenticationFilter.nonceExpired", "Nonce has expired/timed out"); this.fail(request, response, new NonceExpiredException(message)); } else { logger.debug(LogMessage.format("Authentication success for user: '%s' with response: '%s'", digestAuth.getUsername(), digestAuth.getResponse())); Authentication authentication = this.createSuccessfulAuthentication(request, user); SecurityContext context = SecurityContextHolder.createEmptyContext(); context.setAuthentication(authentication); SecurityContextHolder.setContext(context); chain.doFilter(request, response); } } else { chain.doFilter(request, response); } }
DigestData
為摘要資料:
private class DigestData { //使用者名稱 private final String username; //認證域 private final String realm; //隨機字串 private final String nonce; private final String uri; private final String response; //保護級別 private final String qop; //即nonce-count, 指請求的次數, 用於計數, 防止重放攻擊 private final String nc; private final String cnonce; private final String section212response; private long nonceExpiryTime; DigestData(String header) { this.section212response = header.substring(7); String[] headerEntries = DigestAuthUtils.splitIgnoringQuotes(this.section212response, ','); Map<String, String> headerMap = DigestAuthUtils.splitEachArrayElementAndCreateMap(headerEntries, "=", """); this.username = (String)headerMap.get("username"); this.realm = (String)headerMap.get("realm"); this.nonce = (String)headerMap.get("nonce"); this.uri = (String)headerMap.get("uri"); this.response = (String)headerMap.get("response"); this.qop = (String)headerMap.get("qop"); this.nc = (String)headerMap.get("nc"); this.cnonce = (String)headerMap.get("cnonce"); DigestAuthenticationFilter.logger.debug(LogMessage.format("Extracted username: '%s'; realm: '%s'; nonce: '%s'; uri: '%s'; response: '%s'", new Object[]{this.username, this.realm, this.nonce, this.uri, this.response})); } //驗證和解密 void validateAndDecode(String entryPointKey, String expectedRealm) throws BadCredentialsException { if (this.username != null && this.realm != null && this.nonce != null && this.uri != null && this.response != null) { if ("auth".equals(this.qop) && (this.nc == null || this.cnonce == null)) { DigestAuthenticationFilter.logger.debug(LogMessage.format("extracted nc: '%s'; cnonce: '%s'", this.nc, this.cnonce)); throw new BadCredentialsException(DigestAuthenticationFilter.this.messages.getMessage("DigestAuthenticationFilter.missingAuth", new Object[]{this.section212response}, "Missing mandatory digest value; received header {0}")); } else if (!expectedRealm.equals(this.realm)) { throw new BadCredentialsException(DigestAuthenticationFilter.this.messages.getMessage("DigestAuthenticationFilter.incorrectRealm", new Object[]{this.realm, expectedRealm}, "Response realm name '{0}' does not match system realm name of '{1}'")); } else { byte[] nonceBytes; try { nonceBytes = Base64.getDecoder().decode(this.nonce.getBytes()); } catch (IllegalArgumentException var8) { throw new BadCredentialsException(DigestAuthenticationFilter.this.messages.getMessage("DigestAuthenticationFilter.nonceEncoding", new Object[]{this.nonce}, "Nonce is not encoded in Base64; received nonce {0}")); } String nonceAsPlainText = new String(nonceBytes); String[] nonceTokens = StringUtils.delimitedListToStringArray(nonceAsPlainText, ":"); if (nonceTokens.length != 2) { throw new BadCredentialsException(DigestAuthenticationFilter.this.messages.getMessage("DigestAuthenticationFilter.nonceNotTwoTokens", new Object[]{nonceAsPlainText}, "Nonce should have yielded two tokens but was {0}")); } else { try { this.nonceExpiryTime = new Long(nonceTokens[0]); } catch (NumberFormatException var7) { throw new BadCredentialsException(DigestAuthenticationFilter.this.messages.getMessage("DigestAuthenticationFilter.nonceNotNumeric", new Object[]{nonceAsPlainText}, "Nonce token should have yielded a numeric first token, but was {0}")); } String expectedNonceSignature = DigestAuthUtils.md5Hex(this.nonceExpiryTime + ":" + entryPointKey); if (!expectedNonceSignature.equals(nonceTokens[1])) { throw new BadCredentialsException(DigestAuthenticationFilter.this.messages.getMessage("DigestAuthenticationFilter.nonceCompromised", new Object[]{nonceAsPlainText}, "Nonce token compromised {0}")); } } } } else { throw new BadCredentialsException(DigestAuthenticationFilter.this.messages.getMessage("DigestAuthenticationFilter.missingMandatory", new Object[]{this.section212response}, "Missing mandatory digest value; received header {0}")); } } //計算服務摘要 String calculateServerDigest(String password, String httpMethod) { //生產摘要 return DigestAuthUtils.generateDigest(DigestAuthenticationFilter.this.passwordAlreadyEncoded, this.username, this.realm, password, httpMethod, this.uri, this.qop, this.nonce, this.nc, this.cnonce); } //判斷亂數是否到期 boolean isNonceExpired() { long now = System.currentTimeMillis(); return this.nonceExpiryTime < now; } String getUsername() { return this.username; } String getResponse() { return this.response; } }
到此這篇關於Spring Security實現HTTP認證的文章就介紹到這了,更多相關Spring Security HTTP認證內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45