Spring Security的介紹就省略了,直接記錄一下登陸驗證授權的過程。 Spring Security的幾個重要詞 1.SecurityContextHolder:是安全上下文容器,可以在此得知操作的用戶是誰,該用戶是否已經被認證,他擁有哪些角色許可權…這些都被保存在SecurityConte ...
Spring Security的介紹就省略了,直接記錄一下登陸驗證授權的過程。
Spring Security的幾個重要詞
1.SecurityContextHolder:是安全上下文容器,可以在此得知操作的用戶是誰,該用戶是否已經被認證,他擁有哪些角色許可權…這些都被保存在SecurityContextHolder中。
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if (principal instanceof UserDetails) { String username = ((UserDetails)principal).getUsername(); } else { String username = principal.toString();
上面的代碼是通過SecurityContextHolder來獲取到信息,其中getAuthentication()返回了認證信息,再次getPrincipal()返回了身份信息,UserDetails便是Spring對身份信息封裝的一個介面。
2.Authentication:源碼如下:
package org.springframework.security.core; import java.io.Serializable; import java.security.Principal; import java.util.Collection; public interface Authentication extends Principal, Serializable { Collection<? extends GrantedAuthority> getAuthorities(); Object getCredentials(); Object getDetails(); Object getPrincipal(); boolean isAuthenticated(); void setAuthenticated(boolean var1) throws IllegalArgumentException; }
- getAuthorities(),許可權信息列表,預設是GrantedAuthority介面的一些實現類,通常是代表許可權信息的一系列字元串。
- getCredentials(),密碼信息,用戶輸入的密碼字元串,在認證過後通常會被移除,用於保障安全。
- getDetails(),細節信息,web應用中的實現介面通常為 WebAuthenticationDetails,它記錄了訪問者的ip地址和sessionId的值。
- getPrincipal(),敲黑板!!!最重要的身份信息,大部分情況下返回的是UserDetails介面的實現類,也是框架中的常用介面之一。
3.AuthenticationManager:顧名思義,它是認證的一個管理者他是一個介面,裡面有個方法authenticate接受Authentication這個參數來完成驗證;
4.ProviderManager實現AuthenticationManager這個介面,完成驗證工作。部分源碼:
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean { // 維護一個AuthenticationProvider列表 private List<AuthenticationProvider> providers = Collections.emptyList(); public Authentication authenticate(Authentication authentication) throws AuthenticationException { Class<? extends Authentication> toTest = authentication.getClass(); AuthenticationException lastException = null; Authentication result = null; boolean debug = logger.isDebugEnabled(); Iterator var6 = this.getProviders().iterator(); //依次來認證 while(var6.hasNext()) { AuthenticationProvider provider = (AuthenticationProvider)var6.next(); if (provider.supports(toTest)) { if (debug) { logger.debug("Authentication attempt using " + provider.getClass().getName()); } try { // 如果有Authentication信息,則直接返回 result = provider.authenticate(authentication); if (result != null) { this.copyDetails(authentication, result); break; } } catch (AccountStatusException var11) { this.prepareException(var11, authentication); throw var11; } catch (InternalAuthenticationServiceException var12) { this.prepareException(var12, authentication); throw var12; } catch (AuthenticationException var13) { lastException = var13; } } } }
5.
DaoAuthenticationProvider:它是AuthenticationProvider的的一個實現類,非常重要,它主要完成了兩個工作,
5.
一個是retrieveUser方法,它返回UserDetails類,看看它的源碼:
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { UserDetails loadedUser; try { //記住loadUserByUsername這個方法; loadedUser = this.getUserDetailsService().loadUserByUsername(username); } catch (UsernameNotFoundException var6) { if (authentication.getCredentials() != null) { String presentedPassword = authentication.getCredentials().toString(); this.passwordEncoder.isPasswordValid(this.userNotFoundEncodedPassword, presentedPassword, (Object)null); } throw var6; } catch (Exception var7) { throw new InternalAuthenticationServiceException(var7.getMessage(), var7); } if (loadedUser == null) { throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation"); } else { return loadedUser; } }
它還有一個重要的方法是
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { Object salt = null; if (this.saltSource != null) {、 //此方法在你的配置文件中去配置實現的 也是spring security加密的關鍵 ------劃重點 salt = this.saltSource.getSalt(userDetails); } if (authentication.getCredentials() == null) { this.logger.debug("Authentication failed: no credentials provided"); throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } else { String presentedPassword = authentication.getCredentials().toString(); if (!this.passwordEncoder.isPasswordValid(userDetails.getPassword(), presentedPassword, salt)) { this.logger.debug("Authentication failed: password does not match stored value"); throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } } }
這個方法的坑點還是挺多的,主要的意思就是拿到通過用戶姓名獲得的該用戶的信息(密碼等)和用戶輸入的密碼加密後對比,如果不正確就會報錯Bad credentials的錯誤。
為什麼說這個方法坑,因為註意到
this.passwordEncoder.isPasswordValid(userDetails.getPassword(), presentedPassword, salt)
這裡面他自帶的一個方法用的是MD5的加密幫你加密在和你存入這個用戶時的密碼對比,
public boolean isPasswordValid(String encPass, String rawPass, Object salt) { String pass1 = encPass + ""; String pass2 = this.mergePasswordAndSalt(rawPass, salt, false); if (this.ignorePasswordCase) { pass1 = pass1.toLowerCase(Locale.ENGLISH); pass2 = pass2.toLowerCase(Locale.ENGLISH); } return PasswordEncoderUtils.equals(pass1, pass2); }
可以註意到在生成pass2的時候傳入了salt對象,這個salt對象可以通過配置文件去實現,也可以自己寫一個實現類來完成。可以說是是和用戶輸入密碼匹配的關鍵點所在。
6.UserDetails與UserDetailsService,這兩個介面在上面都出現了,先看UserDetails是什麼:
package org.springframework.security.core.userdetails; import java.io.Serializable; import java.util.Collection; import org.springframework.security.core.GrantedAuthority; public interface UserDetails extends Serializable { Collection<? extends GrantedAuthority> getAuthorities(); String getPassword(); String getUsername(); boolean isAccountNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled(); }
有沒有發現它和前面的Authentication介面很像,比如它們都擁有username,authorities,區分他們也是本文的重點內容之一。
Authentication的getCredentials()與UserDetails中的getPassword()需要被區分對待,前者是用戶提交的密碼憑證,後者是用戶正確的密碼,
認證器其實就是對這兩者的比對。Authentication中的getAuthorities()實際是由UserDetails的getAuthorities()傳遞而形成的。
還記得Authentication介面中的getUserDetails()方法嗎?其中的UserDetails用戶詳細信息便是經過了AuthenticationProvider之後被填充的。
public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }
UserDetailsService和AuthenticationProvider兩者的職責常常被人們搞混,關於他們的問題在文檔的FAQ和issues中屢見不鮮。記住一點即可,敲黑板!!!UserDetailsService只負責從特定的地方(通常是資料庫)載入用戶信息,僅此而已,記住這一點,可以避免走很多彎路。UserDetailsService常見的實現類有JdbcDaoImpl,InMemoryUserDetailsManager,前者從資料庫載入用戶,後者從記憶體中載入用戶,也可以自己實現UserDetailsService,通常這更加靈活。
ok,到此我們可以來走一遍流程了。
首先我們得有一個pojo對象,去實現UserDetail得介面,繼承一下幾個方法
@Override public Collection<? extends GrantedAuthority> getAuthorities() { if(roles == null || roles.size()<=0){ return null; } List<SimpleGrantedAuthority> authorities = new ArrayList<SimpleGrantedAuthority>(); for(Role r:roles){ authorities.add(new SimpleGrantedAuthority(r.getRoleValue())); } return authorities; } public String getPassword() { return password; } @Override public String getUsername() { return email; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { if(StringUtils.isNotBlank(state) && "1".equals(state) && StringUtils.isNotBlank(enable) && "1".equals(enable)){ return true; } return false; } @Override public boolean equals(Object obj) { if (obj instanceof User) { return getEmail().equals(((User)obj).getEmail())||getUsername().equals(((User)obj).getUsername()); } return false; } @Override public int hashCode() { return getUsername().hashCode(); }
(1)其中 getAuthorities 方法是獲取用戶角色信息的方法,用於授權。不同的角色可以擁有不同的許可權。
(2)賬戶未過期、賬戶未鎖定和密碼未過期我們這裡沒有用到,直接返回 True,你也可以根據自己的應用場景寫自己的業務邏輯。
(3)為了區分是否是同一個用戶,重寫 equals 和 hashCode 方法。
因為實現介面之後可以獲得資料庫中的真是存在的信息;
使用這個框架之間我們要引入它,首先要在web.xml文件中引入它
<filter> <filter-name>springSecurityFilterChain</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>springSecurityFilterChain</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
然後UsernamePasswordAuthenticationFilter這個過濾器會接受到此方法,在源碼裡面已經幫我們實現獲得密碼以及用戶名的操作,並且規定post請求方法
具體代碼如下:
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (this.postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } else { String username = this.obtainUsername(request); String password = this.obtainPassword(request); if (username == null) { username = ""; } if (password == null) { password = ""; } username = username.trim(); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); this.setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } }
在現實生活中,開發中可以增加的邏輯很多,所以一般都會重寫這個方法;我們要建一個自己的類去繼承這個類:
public class AccountAuthenticationFilter extends UsernamePasswordAuthenticationFilter { private String codeParameter = "code"; @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { String username = this.obtainUsername(request); String password = this.obtainPassword(request); String code = this.obtainCode(request); String caChecode = (String)request.getSession().getAttribute("VERCODE_KEY"); boolean flag = CodeValidate.validateCode(code,caChecode); if(!flag){ throw new UsernameNotFoundException("驗證碼錯誤"); } if(username == null) { username = ""; } if(password == null) { password = ""; } username = username.trim(); //通過構造方法實例化一個 UsernamePasswordAuthenticationToken 對象,此時調用的是 UsernamePasswordAuthenticationToken 的兩個參數的構造函數 //其中 super(null) 調用的是父類的構造方法,傳入的是許可權集合,因為目前還沒有認證通過,所以不知道有什麼許可權信息,這裡設置為 null,然後將用戶名和密碼分別賦值給 // principal 和 credentials,同樣因為此時還未進行身份認證,所以 setAuthenticated(false)。 UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); //setDetails(request, authRequest) 是將當前的請求信息設置到 UsernamePasswordAuthenticationToken 中。 this.setDetails(request, authRequest); //通過調用 getAuthenticationManager() 來獲取 AuthenticationManager,通過調用它的 authenticate 方法來查找支持該 // token(UsernamePasswordAuthenticationToken) 認證方式的 provider,然後調用該 provider 的 authenticate 方法進行認證)。 return this.getAuthenticationManager().authenticate(authRequest); } protected String obtainCode(HttpServletRequest request) { return request.getParameter(this.codeParameter); } }
裡面我們完成了一個驗證碼的驗證工作,並且把僅為post請求給屏蔽,獲取到用戶名和用戶密碼後,我們把它放在了UsernamePasswordAuthenticationToken類里,進去之後看到了
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) { super((Collection)null); this.principal = principal; this.credentials = credentials; this.setAuthenticated(false); }
代碼中給予了註釋,然後setDetails將其存入UsernamePasswordAuthenticationToken之中,然後我們通過getAuthenticationManager()
獲取AuthenticationManager這個介面,在調用介面里的方法,我們繼續查找會發現AuthenticationManager這個類實現了這個介面的方法,
在方法中它又調用了AuthenticationProvide這個介面,那AuthenticationProvide這個介面的實現類是AbstractUserDetailsAuthenticationProvider
並且實現了authenticate方法,在這個方法裡面引用了兩個重要的方法additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication);
和user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);
那這兩個方法在子類DaoAuthenticationProvider中實現,兩個方法上面都有代碼,但是我們再看一下其中重點的方法
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { UserDetails loadedUser; try { //很關鍵 loadedUser = this.getUserDetailsService().loadUserByUsername(username); } catch (UsernameNotFoundException var6) { if (authentication.getCredentials() != null) { String presentedPassword = authentication.getCredentials().toString(); this.passwordEncoder.isPasswordValid(this.userNotFoundEncodedPassword, presentedPassword, (Object)null); } throw var6; } catch (Exception var7) { throw new InternalAuthenticationServiceException(var7.getMessage(), var7); } if (loadedUser == null) { throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation"); } else { return loadedUser; } }
那個註釋的地方是要獲得一個UserDetails,上面有說到UserDetailsService常見的實現類有JdbcDaoImpl,InMemoryUserDetailsManager,為了簡化我們自己寫一個實現類,
因為結合我們pojo對象實現了UserDetails的介面,所以我們創建如下類:
public class AccountDetailsService implements UserDetailsService{ @Autowired private UserService userService; @Autowired private RoleService roleService; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userService.findByEmail(username); if(user == null){ throw new UsernameNotFoundException("用戶名或密碼錯誤"); } List<Role> roles = roleService.findByUid(user.getId()); user.setRoles(roles); return user; } }
實現了loadByUsername的方法。到此為止我們我們在逆向的回到了UsernamePasswordAuthenticationFilter上,且返回了一個Authentication對象。
我們在第一個關鍵詞SecurityContextHolder中將其取出,做一些自己的業務邏輯。
工作到此還沒有結束,我們還要去授權,對認證通過的人去授權,這裡我們可以xml去配置這些信息:我們前面留了一個問題就是salt加密密碼驗證,我們前面還不知道salt
對象是什麼,所以需要配置一下
<!-- 認證管理器,使用自定義的accountService,並對密碼採用md5加密 --> <security:authentication-manager alias="authenticationManager"> <security:authentication-provider user-service-ref="accountService"> <security:password-encoder hash="md5"> <security:salt-source user-property="username"></security:salt-source> </security:password-encoder> </security:authentication-provider> </security:authentication-manager>
其實salt可以自己代碼去配置,通過這個xml去配置也行,最緊要的還是要和你原來資料庫密碼的加密方式有關係,我這裡是用了pojo對象里的用戶名作為salt對象,
所以我的密碼加密方式就是username+password再用MD5加密了。那還有一個重要的工作就是授權配置
<security:http security="none" pattern="/css/**" /> <security:http security="none" pattern="/js/**" /> <security:http security="none" pattern="/images/**" /> <security:intercept-url pattern="/" access="permitAll"/> <security:intercept-url pattern="/index**" access="permitAll"/> <security:intercept-url pattern="/**" access="hasRole('ROLE_USER')"/>
這些都是基礎的一些授權操作,還有配置在我們的AccountAuthenticationFilter類中是不是通過了驗證
<bean id="authenticationFilter" class="***.***.**.**.AccountAuthenticationFilter"> <property name="filterProcessesUrl" value="/doLogin"></property> <property name="authenticationManager" ref="authenticationManager"></property> <property name="sessionAuthenticationStrategy" ref="sessionStrategy"></property> <property name="authenticationSuccessHandler"> <bean class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler"> <property name="defaultTargetUrl" value="/list"></property> </bean> </property> <property name="authenticationFailureHandler"> <bean class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler"> <property name="defaultFailureUrl" value="/login.jsp?error=fail"></property> </bean> </property> </bean>
其中defaultTargetUrl和defaultFailureUrl是通過和不通過的一些採取措施,通常是一些頁面跳轉。
其餘的配置文件信息,我還沒有琢磨透,以後有時間在發表一篇。
最後:用一張圖大致的總結下它的具體流程(本圖來自王林永老師的gitchat):