自定義用戶認證邏輯
處理用戶信息獲取邏輯
實現UserDetailsService接口
@Service
public class MyUserDetailsService implements UserDetailsService {
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
logger.info("根據用戶名查找用戶信息,登錄用戶名:" + username);
// 從數據庫查詢相關的密碼和權限,這里返回一個假的數據
// 用戶名,密碼,權限
return new User(username,
"123456",
AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
處理用戶校驗邏輯
UserDetails接口的一些方法,封裝了登錄時的一些信息
public interface UserDetails extends Serializable {
/** 權限信息
* Returns the authorities granted to the user. Cannot return <code>null</code>.
*
* @return the authorities, sorted by natural key (never <code>null</code>)
*/
Collection<? extends GrantedAuthority> getAuthorities();
/** 密碼
* Returns the password used to authenticate the user.
*
* @return the password
*/
String getPassword();
/** 登錄名
* Returns the username used to authenticate the user. Cannot return <code>null</code>
* .
*
* @return the username (never <code>null</code>)
*/
String getUsername();
/** 賬戶是否過期
* Indicates whether the user's account has expired. An expired account cannot be
* authenticated.
*
* @return <code>true</code> if the user's account is valid (ie non-expired),
* <code>false</code> if no longer valid (ie expired)
*/
boolean isAccountNonExpired();
/** 賬戶是否被鎖定(凍結)
* Indicates whether the user is locked or unlocked. A locked user cannot be
* authenticated.
*
* @return <code>true</code> if the user is not locked, <code>false</code> otherwise
*/
boolean isAccountNonLocked();
/** 密碼是否過期
* Indicates whether the user's credentials (password) has expired. Expired
* credentials prevent authentication.
*
* @return <code>true</code> if the user's credentials are valid (ie non-expired),
* <code>false</code> if no longer valid (ie expired)
*/
boolean isCredentialsNonExpired();
/** 賬戶是否可用(刪除)
* Indicates whether the user is enabled or disabled. A disabled user cannot be
* authenticated.
*
* @return <code>true</code> if the user is enabled, <code>false</code> otherwise
*/
boolean isEnabled();
}
返回數據寫成
return new User(username, // 用戶名
"123456", // 密碼
true, // 是否可用
true, // 賬號是否過期
true, // 密碼是否過期
true, // 賬號沒有被鎖定標志
AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
處理密碼加密解密
PasswordEncoder接口
public interface PasswordEncoder {
/** 加密
* Encode the raw password. Generally, a good encoding algorithm applies a SHA-1 or
* greater hash combined with an 8-byte or greater randomly generated salt.
*/
String encode(CharSequence rawPassword);
/** 判斷密碼是否匹配
* Verify the encoded password obtained from storage matches the submitted raw
* password after it too is encoded. Returns true if the passwords match, false if
* they do not. The stored password itself is never decoded.
*
* @param rawPassword the raw password to encode and match
* @param encodedPassword the encoded password from storage to compare with
* @return true if the raw password, after encoding, matches the encoded password from
* storage
*/
boolean matches(CharSequence rawPassword, String encodedPassword);
}
在BrowerSecurityConfig中配置PasswordEncoder
// 配置PasswordEncoder
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
MyUserDetailsService.java改成
// 注入passwordEncoder
@Autowired
private PasswordEncoder passwordEncoder;
// 返回寫成這樣
return new User(username, // 用戶名
passwordEncoder.encode("123456"), // 這個是從數據庫中讀取的已加密的密碼
true, // 是否可用
true, // 賬號是否過期
true, // 密碼是否過期
true, // 賬號沒有被鎖定標志
AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
個性化用戶認證流程
自定義登錄頁面
修改BrowserSecurityConfig類
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
// 配置PasswordEncoder
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
System.out.println("BrowserSecurityConfig");
http.formLogin() // 表單登錄
.loginPage("/sign.html") // 自定義登錄頁面URL
.loginProcessingUrl("/authentication/form") // 處理登錄請求的URL
.and()
.authorizeRequests() // 對請求做授權
.antMatchers("/sign.html").permitAll() // 登錄頁面不需要認證
.anyRequest() // 任何請求
.authenticated() // 都需要身份認證
.and().csrf().disable(); // 暫時將防護跨站請求偽造的功能置為不可用
}
}
問題
- 不同的登錄方式,通過頁面登錄,通過app登錄
- 給多個應用提供認證服務,每個應用需要的自定義登錄頁面
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
// 配置PasswordEncoder
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
System.out.println("BrowserSecurityConfig");
http.formLogin() // 表單登錄
.loginPage("/authentication/require") // 自定義登錄頁面URL
.loginProcessingUrl("/authentication/form") // 處理登錄請求的URL
.and()
.authorizeRequests() // 對請求做授權
.antMatchers("/authentication/require",
securityProperties.getBrowser().getLoginPage())
.permitAll() // 登錄頁面不需要認證
.anyRequest() // 任何請求
.authenticated() // 都需要身份認證
.and().csrf().disable(); // 暫時將防護跨站請求偽造的功能置為不可用
}
}
BrowserSecurityController判斷訪問的url如果以.html結尾就跳轉到登錄頁面,否則就返回json格式的提示信息
@RestController
public class BrowserSecurityController {
private Logger logger = LoggerFactory.getLogger(getClass());
private RequestCache requestCache = new HttpSessionRequestCache();
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Autowired
private SecurityProperties securityProperties;
/**
* 需要身份認證時,跳轉到這里
*
* @param request
* @param response
* @return
*/
@RequestMapping("/authentication/require")
@ResponseStatus(code = HttpStatus.UNAUTHORIZED)
public SimpleResponse requireAuthentication(HttpServletRequest request,
HttpServletResponse response)
throws IOException {
SavedRequest savedRequest = requestCache.getRequest(request, response);
if (savedRequest != null) {
String targetUrl = savedRequest.getRedirectUrl();
logger.info("引發跳轉請求的url是:" + targetUrl);
if (StringUtils.endsWithIgnoreCase(targetUrl, ".html")) {
redirectStrategy.sendRedirect(request, response,
securityProperties.getBrowser().getLoginPage());
}
}
return new SimpleResponse("訪問的服務需要身份認證,請引導用戶到登錄頁");
}
}
自定義登錄成功處理
AuthenticationSuccessHandler接口,此接口登錄成功后會被調用
@Component
public class ImoocAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private Logger logger = LoggerFactory.getLogger(ImoocAuthenticationSuccessHandler.class);
@Autowired
private ObjectMapper objectMapper;
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication)
throws IOException, ServletException {
logger.info("登錄成功");
// 登錄成功后把authentication返回給前台
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(authentication));
}
}
自定義登錄失敗處理
@Component
public class ImoocAuthenticationFailHandler implements AuthenticationFailureHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private ObjectMapper objectMapper;
@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException e)
throws IOException, ServletException {
logger.info("登錄失敗");
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(e));
}
}
問題
- 登錄成功或失敗后返回頁面還是json數據格式
登錄成功后的處理
@Component
public class ImoocAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
private Logger logger = LoggerFactory.getLogger(ImoocAuthenticationSuccessHandler.class);
@Autowired
private ObjectMapper objectMapper;
@Autowired
private SecurityProperties securityProperties;
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication)
throws IOException, ServletException {
logger.info("登錄成功");
if (LoginType.JSON.equals(securityProperties.getBrowser().getLoginType())) {
// 登錄成功后把authentication返回給前台
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(authentication));
} else {
super.onAuthenticationSuccess(request, response, authentication);
}
}
}
登錄失敗后的處理
@Component
public class ImoocAuthenticationFailHandler extends SimpleUrlAuthenticationFailureHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private ObjectMapper objectMapper;
@Autowired
private SecurityProperties securityProperties;
@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException e)
throws IOException, ServletException {
logger.info("登錄失敗");
if (LoginType.JSON.equals(securityProperties.getBrowser().getLoginType())) {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(e));
} else {
super.onAuthenticationFailure(request, response, e);
}
}
}
認證流程源碼級詳解
認證處理流程說明
認證結果如何在多個請求之間共享
一個請求進來的時候,先檢查context是否存有該請求的認證信息
獲取認證用戶信息
圖片驗證碼
生成圖片驗證碼
- 根據隨機數生成圖片
- 將隨機數存到Session中
- 在將生成的圖片寫到接口的響應中
圖片驗證碼重構
驗證碼基本參數可配置
驗證碼圖片的寬,高,字符數,失效時間可配置(注意字符數和失效時間不要在請求級配置中)。請求級配置就是在請求驗證碼時/code/image?width=100&height=30,應用級配置就是在應用的配置文件中
// 在使用這些配置時,如果請求級配置有就用請求級配置,否則就依次用應用級配置,默認配置
int width = ServletRequestUtils.getIntParameter(request.getRequest(), "width",
securityProperties.getCode().getImage().getWidth());
int height = ServletRequestUtils.getIntParameter(request.getRequest(), "height",
securityProperties.getCode().getImage().getHeight());
驗證碼攔截的接口可配置
默認情況下,只有在注冊,登錄的需要驗證碼的時候才攔截的,如果還有其他情景下需要則能夠在不修改依賴的情況下可配置.如何實現呢,在配置文件中添加要需要驗證碼的url,驗證碼的驗證是通過過濾器實現的,那么在對其過濾的時候判斷當前url是否是需要攔截即可
驗證碼的生成邏輯可配置
把生成驗證碼的功能定義成接口,框架給出一個默認的實現,如果應用不定義就用這個默認實現,如果應用要定制一個,就實現這個接口就可以了.
// 框架中的默認實現不加注釋@Component進行初始化,用如下方式對其進行初始化
// 檢測上下文環境中是否有imageCodeGenerator這個bean,如果沒有就初始化框架中提供的默認實現
@Configuration
public class ValidateCodeBeanConfig {
@Autowired
private SecurityProperties securityProperties;
@Bean
@ConditionalOnMissingBean(name = "imageCodeGenerator")
public ValidateCodeGenerator imageCodeGenerator() {
System.out.println("init imageCodeGenerator");
ImageCodeGenerator codeGenerator = new ImageCodeGenerator();
codeGenerator.setSecurityProperties(securityProperties);
return codeGenerator;
}
}
添加記住我功能
基本原理
具體實現
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
// 用來讀取配置
@Autowired
private SecurityProperties securityProperties;
// 登錄成功后的處理
@Autowired
private ImoocAuthenticationSuccessHandler imoocAuthenticationSuccessHandler;
// 登錄失敗后的處理
@Autowired
private ImoocAuthenticationFailHandler imoocAuthenticationFailHandler;
@Autowired
private DataSource dataSource;
@Autowired
private UserDetailsService userDetailsService;
// 配置PasswordEncoder
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 用於remember me
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
// tokenRepository.setCreateTableOnStartup(true); // 啟動時創建表
return tokenRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
System.out.println("BrowserSecurityConfig");
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
validateCodeFilter.setAuthenticationFailureHandler(imoocAuthenticationFailHandler);
validateCodeFilter.setSecurityProperties(securityProperties);
validateCodeFilter.afterPropertiesSet();
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin() // 表單登錄
.loginPage("/authentication/require") // 自定義登錄頁面URL
.loginProcessingUrl("/authentication/form") // 處理登錄請求的URL
.successHandler(imoocAuthenticationSuccessHandler) // 登錄成功后的處理
.failureHandler(imoocAuthenticationFailHandler) // 登錄失敗后的處理
.and()
.rememberMe()
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds())
.userDetailsService(userDetailsService)
.and()
.authorizeRequests() // 對請求做授權
.antMatchers("/authentication/require",
securityProperties.getBrowser().getLoginPage(),
"/code/image")
.permitAll() // 登錄頁面不需要認證
.anyRequest() // 任何請求
.authenticated() // 都需要身份認證
.and().csrf().disable(); // 暫時將防護跨站請求偽造的功能置為不可用
}
}
源碼解析
略