本文用示例的方式講解,springsecurity,使用session方式,
用戶名密碼和手機(jī)驗(yàn)證碼兩種方式
非常簡陋的登入頁面
該示例的代碼
CustomAuthenticationFailureHandler 失敗處理器
/**
認(rèn)證失敗處理器
**/
@Component
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException{
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("認(rèn)證失敗");
}
}
CustomAuthenticationSuccessHandler 成功處理器
/**
認(rèn)證成功處理器
**/
@Component
public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException{
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("認(rèn)證成功");
}
}
CustomUserDetailsService 獲取用戶信息
/**
*
* 模擬從數(shù)據(jù)庫獲取用戶信息,這里要注意的是,如果在配置的時(shí)候使用內(nèi)存的方式,是不回使用該services
* SpringSecurityConfiguration方法中規(guī)定了使用那種方式管理用戶信息,本例使用的是內(nèi)存的方式
* 所以在用戶名密碼模式的時(shí)候,不回執(zhí)行l(wèi)oadUserByUsername,手機(jī)登入的時(shí)候還是會(huì)走loadUserByUsername方法
*/
@Configuration
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException{
//封裝用戶信息,用戶名和密碼和權(quán)限,注意這里要注意密碼應(yīng)該是加密的
//省略從數(shù)據(jù)庫獲取詳細(xì)信息
return new User(username, "1234",
AuthorityUtils.commaSeparatedStringToAuthorityList("ADMIN"));
}
}
SpringSecurityConfiguration security整體配置
@Configuration
@EnableWebSecurity
public class SpringSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
SecurityConfigurerAdapter mobileAuthenticationConfig;
@Override
protected void configure(HttpSecurity http) throws Exception{
http.csrf().disable()
.authorizeRequests()
.antMatchers("/login.html","/code/mobile").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.successHandler(new CustomAuthenticationSuccessHandler())
.failureHandler(new CustomAuthenticationFailureHandler())
.loginPage("/login")
; //瀏覽器以form表單形式
//將手機(jī)驗(yàn)證碼配置放到http中,這樣mobileAuthenticationConfig配置就會(huì)生效
http.apply(mobileAuthenticationConfig);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
// 用戶信息存儲(chǔ)在內(nèi)存中
auth.inMemoryAuthentication().withUser("user")
.password(new BCryptPasswordEncoder().encode("1234")).authorities("ADMIN");
}
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers("/code/mobile");
}
@Bean
public PasswordEncoder passwordEncoder(){
// 官網(wǎng)建議的加密方式,相同的密碼,每次加密都不一樣,安全性更好一點(diǎn)
return new BCryptPasswordEncoder();
}
}
CacheValidateCode 手機(jī)驗(yàn)證碼的內(nèi)存存儲(chǔ)
/**
* 將手機(jī)驗(yàn)證碼保存起來,后續(xù)驗(yàn)證中,實(shí)際項(xiàng)目中要放到redis等存儲(chǔ)
**/
public class CacheValidateCode {
public static ConcurrentHashMap<String, String> cacheValidateCodeHashMap = new ConcurrentHashMap();
}
MobileAuthenticationConfig 手機(jī)驗(yàn)證碼配置類,在SpringSecurityConfiguration中通過http.apply方式放到springsecurity中
/**
* 用于組合其他關(guān)于手機(jī)登錄的組件
*/
@Component
public class MobileAuthenticationConfig
extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
@Autowired
CustomAuthenticationFailureHandler customAuthenticationFailureHandler;
@Autowired
UserDetailsService mobileUserDetailsService;
@Override
public void configure(HttpSecurity http) throws Exception{
MobileAuthenticationFilter mobileAuthenticationFilter = new MobileAuthenticationFilter();
// 獲取容器中已經(jīng)存在的AuthenticationManager對(duì)象昂芜,并傳入 mobileAuthenticationFilter 里面
mobileAuthenticationFilter.setAuthenticationManager(
http.getSharedObject(AuthenticationManager.class));
// 傳入 失敗與成功處理器
mobileAuthenticationFilter.setAuthenticationSuccessHandler(customAuthenticationSuccessHandler);
mobileAuthenticationFilter.setAuthenticationFailureHandler(customAuthenticationFailureHandler);
// 構(gòu)建一個(gè)MobileAuthenticationProvider實(shí)例锋边,接收 mobileUserDetailsService 通過手機(jī)號(hào)查詢用戶信息
MobileAuthenticationProvider provider = new MobileAuthenticationProvider();
provider.setUserDetailsService(mobileUserDetailsService);
// 將provider綁定到 HttpSecurity上懂傀,并將 手機(jī)號(hào)認(rèn)證過濾器綁定到用戶名密碼認(rèn)證過濾器之后
http.authenticationProvider(provider)
.addFilterAfter(mobileAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
MobileAuthenticationFilter 手機(jī)驗(yàn)證filter,完全模仿UsernamePasswordAuthenticationFilter
/**
* 用于校驗(yàn)用戶手機(jī)號(hào)是否允許通過認(rèn)證
* 完全復(fù)制 UsernamePasswordAuthenticationFilter
*/
public class MobileAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private String mobileParameter = "mobile";
private String validateCodeParameter = "code";
private boolean postOnly = true;
public MobileAuthenticationFilter(){
super(new AntPathRequestMatcher("/mobile/form", "POST"));
}
// ~ Methods
// ========================================================================================================
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException{
if(postOnly && !request.getMethod().equals("POST")){
throw new AuthenticationServiceException(
"Authentication method not supported: "+request.getMethod());
}
String mobile = obtainMobile(request);
String validateCode = obtainValidateCode(request);
if(mobile == null){
mobile = "";
}
mobile = mobile.trim();
MobileAuthenticationToken authRequest = new MobileAuthenticationToken(mobile, validateCode);
// sessionID, hostname
setDetails(request, authRequest);
//認(rèn)證手機(jī)碼是否正確,通過provider的方式處理,使用哪個(gè)provider,是根據(jù)authRequest是哪個(gè)類型的token
//這里放的是MobileAuthenticationToken
return this.getAuthenticationManager().authenticate(authRequest);
}
/**
* 從請(qǐng)求中獲取手機(jī)號(hào)碼
*/
@Nullable
protected String obtainMobile(HttpServletRequest request){
return request.getParameter(mobileParameter);
}
/**
* 從請(qǐng)求中獲取驗(yàn)證碼
*/
@Nullable
protected String obtainValidateCode(HttpServletRequest request){
return request.getParameter(validateCodeParameter);
}
/**
* 將 sessionID和hostname添加 到MobileAuthenticationToken
*/
protected void setDetails(HttpServletRequest request,
MobileAuthenticationToken authRequest){
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
/**
* 設(shè)置是否為post請(qǐng)求
*/
public void setPostOnly(boolean postOnly){
this.postOnly = postOnly;
}
public String getMobileParameter(){
return mobileParameter;
}
public void setMobileParameter(String mobileParameter){
this.mobileParameter = mobileParameter;
}
}
MobileAuthenticationProvider 手機(jī)驗(yàn)證處理器
/**
* 手機(jī)認(rèn)證處理提供者,要注意supports方法和authenticate
* supports判斷是否使用當(dāng)前provider
* authenticate 驗(yàn)證手機(jī)驗(yàn)證碼是否正確
*
*/
public class MobileAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
public void setUserDetailsService(UserDetailsService userDetailsService){
this.userDetailsService = userDetailsService;
}
/**
* 認(rèn)證處理:
* 1. 通過手機(jī)號(hào)碼 查詢用戶信息( UserDetailsService實(shí)現(xiàn))
* 2. 當(dāng)查詢到用戶信息, 則認(rèn)為認(rèn)證通過,封裝Authentication對(duì)象
*
* @param authentication
* @return
* @throws AuthenticationException
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException{
MobileAuthenticationToken mobileAuthenticationToken =
( MobileAuthenticationToken ) authentication;
// 獲取手機(jī)號(hào)碼
String mobile = ( String ) mobileAuthenticationToken.getPrincipal();
String validateCodeParameter = ( String ) mobileAuthenticationToken.getCredentials();
// 通過 手機(jī)號(hào)碼 查詢用戶信息( UserDetailsService實(shí)現(xiàn))
UserDetails userDetails =
userDetailsService.loadUserByUsername(mobile);
mobileAuthenticationToken.setDetails(userDetails);
// 未查詢到用戶信息
if(userDetails == null){
throw new AuthenticationServiceException("該手機(jī)號(hào)未注冊(cè)");
}
// 1. 判斷 請(qǐng)求是否為手機(jī)登錄,且post請(qǐng)求
try{
// 校驗(yàn)驗(yàn)證碼合法性
validate(mobile, validateCodeParameter);
}catch(AuthenticationException e){
throw new AuthenticationServiceException(e.getMessage());
}
//最終返回認(rèn)證信息,這里要注意的是,返回的token中的authenticated字段要賦值為true
return createSuccessAuthentication(mobileAuthenticationToken);
}
/**
* 通過這個(gè)方法,來選擇對(duì)應(yīng)的Provider, 即選擇MobileAuthenticationProivder
*
* @param authentication
* @return
*/
@Override
public boolean supports(Class<?> authentication){
return MobileAuthenticationToken.class.isAssignableFrom(authentication);
}
private void validate(String mobile, String inpuCode){
// 判斷是否正確
if(StringUtils.isEmpty(inpuCode)){
throw new AuthenticationServiceException("驗(yàn)證碼不能為空");
}
String cacheValidateCode = CacheValidateCode.cacheValidateCodeHashMap.get(mobile);
if(!inpuCode.equalsIgnoreCase(cacheValidateCode)){
throw new AuthenticationServiceException("驗(yàn)證碼輸入錯(cuò)誤");
}
}
protected Authentication createSuccessAuthentication(
Authentication authentication){
// Ensure we return the original credentials the user supplied,
// so subsequent attempts are successful even with encoded passwords.
// Also ensure we return the original getDetails(), so that future
// authentication events after cache expiry contain the details
MobileAuthenticationToken result = new MobileAuthenticationToken(
authentication.getPrincipal(), authentication.getCredentials(),
AuthorityUtils.commaSeparatedStringToAuthorityList("ADMIN"));
result.setDetails(authentication.getDetails());
return result;
}
}
MobileAuthenticationToken 手機(jī)驗(yàn)證碼的token
/**
* 創(chuàng)建自己的token,參考UsernamePasswordAuthenticationToken
*/
public class MobileAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
// ~ Instance fields
// ================================================================================================
private final Object principal;
private Object credentials;
// ~ Constructors
// ===================================================================================================
/**
* This constructor can be safely used by any code that wishes to create a
* <code>UsernamePasswordAuthenticationToken</code>, as the {@link #isAuthenticated()}
* will return <code>false</code>.
*/
public MobileAuthenticationToken(Object principal, Object credentials){
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
/**
* This constructor should only be used by <code>AuthenticationManager</code> or
* <code>AuthenticationProvider</code> implementations that are satisfied with
* producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
* authentication token.
*
* @param principal
* @param credentials
* @param authorities
*/
public MobileAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities){
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true); // must use super, as we override
}
// ~ Methods
// ========================================================================================================
public Object getCredentials(){
return this.credentials;
}
public Object getPrincipal(){
return this.principal;
}
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException{
if(isAuthenticated){
throw new IllegalArgumentException(
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
}
super.setAuthenticated(false);
}
@Override
public void eraseCredentials(){
super.eraseCredentials();
credentials = null;
}
}
controller
@Controller
public class Congtroller {
@RequestMapping("/code/mobile")
@ResponseBody
public String mobileCode(HttpServletRequest request){
// 1. 生成一個(gè)手機(jī)驗(yàn)證碼
String code = RandomStringUtils.randomNumeric(4);
// 2. 將手機(jī)獲取的信息保存到緩存里,實(shí)際應(yīng)用中,可以放到redis中
String mobile = request.getParameter("mobile");
CacheValidateCode.cacheValidateCodeHashMap.put(mobile, code);
System.out.println("手機(jī)驗(yàn)證碼"+code);
return code;
}
}
login.html 登入頁,十分簡單
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登錄頁</title>
</head>
<script src="https://upcdn.b0.upaiyun.com/libs/jquery/jquery-2.0.2.min.js"></script>
<body>
<form action="http://127.0.0.1:8080/login"method="post">
<label for="username">用戶名:</label>
<input type="text" name="username" id="username">
<label for="password">密 碼:</label>
<input type="password" name="password" id="password">
<button type="submit">登錄</button>
</form>
<form action="http://127.0.0.1:8080/mobile/form"method="post">
<label for="mobile">手機(jī)號(hào):</label>
<input type="text" name="mobile" id="mobile">
<label for="sendCode">驗(yàn)證碼:</label>
<input type="text" name="code" id="sendCode">
<button type="submit">登錄</button>
</form>
<button onclick="sendCode()"> 獲取驗(yàn)證碼 </button>
<script>
function sendCode() {
$.ajax(
{
type: "post",
url: "http://127.0.0.1:8080/code/mobile",
data: $("#mobile").serialize(),
success: function (result) {
alert(result);
}
}
)
}
</script>
</body>
</html>
思路非常簡單,就是定義了關(guān)于手機(jī)的驗(yàn)證filter,并放到security中,在通過驗(yàn)證碼登入的時(shí)候,首先創(chuàng)建MobileAuthenticationToken,遍歷所有的provider的時(shí)候,通過support方法獲取到使用哪個(gè)provider,MobileAuthenticationProvider手機(jī)驗(yàn)證provider,驗(yàn)證手機(jī)號(hào)的驗(yàn)證碼是否正確,如果正確就將MobileAuthenticationToken放到SecurityContextHolder中,保存在ThreadLocal變量中,該線程就能使用了,并且將MobileAuthenticationToken的authenticated設(shè)置為true,在security的最后一個(gè)攔截器FilterSecurityInterceptor判斷是都已經(jīng)驗(yàn)證過了,并且判斷角色是否可以訪問當(dāng)前接口,
這樣就是驗(yàn)證的整個(gè)流程,session的方式驗(yàn)證,在登入成功的時(shí)候token放到tomcat的內(nèi)存中了,key就是sessionid,前端將session傳到server時(shí),從tomcat中獲取已經(jīng)驗(yàn)證過的token,這樣就實(shí)現(xiàn)了登入后,其他接口可以正常訪問的流程.