前面做了多個示例,包括使用jdbc和hibernate兩種方式訪問數據庫獲取用戶信息和權限信息,其中一些關鍵步驟如下:
我們在SecurityConfig中配置覆蓋configure方法時候,可以指定authenticationProvider,也可以不需要指定,直接指定userDetailsService。例如:
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { authenticationProvider.setPasswordEncoder(passwordEncoder()); auth.authenticationProvider(authenticationProvider); //auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); }
如果沒有指定
authenticationProvider,則security使用的是實現類DaoAuthenticationProvider。
如果指定自定義的
authenticationProvider,為了方便,我們自定義的
authenticationProvider也是繼承自
DaoAuthenticationProvider,只需要重寫
指定userDetailsService,
authenticate方法,例如:
@Autowired @Qualifier("userDetailsService") @Override public void setUserDetailsService(UserDetailsService userDetailsService) { super.setUserDetailsService(userDetailsService); }
@Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { try { //調用上層驗證邏輯 Authentication auth = super.authenticate(authentication); //如果驗證通過登錄成功則重置嘗試次數, 否則拋出異常 userDetailsDao.resetFailAttempts(authentication.getName()); return auth; } catch (BadCredentialsException e) { //如果驗證不通過,則更新嘗試次數,當超過次數以后拋出賬號鎖定異常 userDetailsDao.updateFailAttempts(authentication.getName()); throw e; } catch (LockedException e){ //該用戶已經被鎖定,則進入這個異常 String error; UserAttempts userAttempts = userDetailsDao.getUserAttempts(authentication.getName()); if(userAttempts != null){ Date lastAttempts = userAttempts.getLastModified(); error = "用戶已經被鎖定,用戶名 : " + authentication.getName() + "最后嘗試登陸時間 : " + lastAttempts; }else{ error = e.getMessage(); } throw new LockedException(error); } }
在此方法中,仍然調用的是上層驗證方法super.authenticate();在這里可以根據不同的驗證異常拋出不同的異常,從而顯示不同的用戶賬號狀態,例如用戶被鎖定、用戶失效、賬號或者密碼過期等,這里例子是多次登錄失敗鎖定了用戶。
下面我們看看security是如何驗證賬號的:
驗證邏輯實現是在類AbstractUserDetailsAuthenticationProvider,此類實現了接口AuthenticationProvider的接口方法
Authentication authenticate(Authentication authentication) throws AuthenticationException;
實現方法中首先獲取security定義的接口UserDetails,先從緩存userCache中獲取,如果不存在,則調用方法retrieveUser。
retrieveUser的方法實現是在類
DaoAuthenticationProvider,這個方法中可以看到
UserDetails loadedUser; try { loadedUser = this.getUserDetailsService().loadUserByUsername(username); } catch (UsernameNotFoundException notFound) { if (authentication.getCredentials() != null) { String presentedPassword = authentication.getCredentials().toString(); passwordEncoder.isPasswordValid(userNotFoundEncodedPassword, presentedPassword, null); } throw notFound; } .....
此處調用的是自定義的UserDetailsService中loadUserByUsername方法。於是可以看出自定義的UserDetailsService實現類關鍵是實現loadUserByUsername方法。
下面就兩種方式的實現進行剖解:
1、使用jdbc方式時,我們自定義的UserDetailsService是繼承了類JdbcDaoImpl,可以發現
JdbcDaoImpl已經實現了接口UserDetailsService,實現了方法
loadUserByUsername。
在實現方法中,關鍵是調用自己定義的兩個方法loadUsersByUsername和createUserDetails。於是自定義的Custom
UserDetailsService類只需要覆寫這兩個方法即可
@Override protected List<UserDetails> loadUsersByUsername(String username) { return getJdbcTemplate().query(super.getUsersByUsernameQuery(), new Object[]{username}, (rs, rowNum) -> { String username1 = rs.getString("username"); String password = rs.getString("password"); boolean enabled = rs.getBoolean("enabled"); boolean accountNonExpired = rs.getBoolean("accountNonExpired"); boolean credentialsNonExpired = rs.getBoolean("credentialsNonExpired"); boolean accountNonLocked = rs.getBoolean("accountNonLocked"); return new User(username1, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, AuthorityUtils.NO_AUTHORITIES); }); } @Override protected UserDetails createUserDetails(String username, UserDetails userFromUserQuery, List<GrantedAuthority> combinedAuthorities) { String returnUsername = userFromUserQuery.getUsername(); if (!super.isUsernameBasedPrimaryKey()) { returnUsername = username; } return new User(returnUsername, userFromUserQuery.getPassword(), userFromUserQuery.isEnabled(), userFromUserQuery.isAccountNonExpired(), userFromUserQuery.isCredentialsNonExpired(), userFromUserQuery.isAccountNonLocked(), combinedAuthorities); }
在這里我們根據需要指定各個值,例如用戶名,密碼,是否可用,賬號和密碼是否過期,是否賬號被鎖等,所以如果已經設計完成的數據表中字段名稱不一致也沒有關系,只要含義相同,獲取值指定即可。
2、使用hibernate方式時,需要自己實現UserDetailsService接口中的方法
loadUserByUsername:
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userDao.findByUserName(username); if (user == null) { throw new UsernameNotFoundException("該用戶不存在:" + username); } List<GrantedAuthority> authorities = buildUserAuthority(user.getUserRole()); return buildUserForAuthentication(user, authorities); } // 把自定義的User轉換成org.springframework.security.core.userdetails.User private org.springframework.security.core.userdetails.User buildUserForAuthentication( User user, List<GrantedAuthority> authorities) { return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), user.isEnabled(), user.isAccountNonExpired(), user.isCredentialsNonExpired(), user.isAccountNonLocked(), authorities); } private List<GrantedAuthority> buildUserAuthority(Set<UserRole> userRoles) { Set<GrantedAuthority> setAuths = new HashSet<>(); // Build user's authorities for (UserRole userRole : userRoles) { setAuths.add(new SimpleGrantedAuthority(userRole.getRole())); } return new ArrayList<>(setAuths); }
在方法中使用hibernate的方式獲取自定義的User實例,然后轉換成security中的org.springframework.security.core.userdetails.User即可,org.springframework.security.core.userdetails.User是接口UserDetails的實現類。
附上所有示例的代碼的github地址:
https://github.com/hongxf1990/spring-security-learning
嘿嘿,如果覺得以上實例項目中可以借鑒的話,不妨打個賞吧

