如果不是前后端分離項目,使用SpringSecurity做登錄功能會很省心,只要簡單的幾項配置,便可以輕松完成登錄成功失敗的處理,當訪問需要認證的頁面時,可以自動重定向到登錄頁面。但是前后端分離的項目就不一樣了,不能直接由后台處理,而是要向前端返回相應的json提示。
在本例的介紹中,主要解決了以下幾個問題:
1.使用json格式數據進行登錄。
2.登錄成功或失敗處理返回json提示。
3.未登錄時訪問需要認證的url時,返回json提示。
4.session過期時返回json提示。
一、引入security依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
二、編寫配置文件
package com.hanstrovsky.config;
...
/**
* @author Hanstrovsky
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // security默認不支持注解的方式的權限控制,加上這個注解開啟
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final MyUserDetailsService myUserDetailsService;
private final MyPasswordEncoder myPasswordEncoder;
public WebSecurityConfig(MyUserDetailsService myUserDetailsService, MyPasswordEncoder myPasswordEncoder) {
this.myUserDetailsService = myUserDetailsService;
this.myPasswordEncoder = myPasswordEncoder;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 定義加密解密方式
auth.userDetailsService(myUserDetailsService).passwordEncoder(myPasswordEncoder);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.httpBasic()
// 訪問需要認證的url,進行json提示
.and().exceptionHandling()
.authenticationEntryPoint((req, resp, e) -> {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
FrontResult frontResult = FrontResult.init(FrontResult.LOGIN, "未登錄或登錄超時!");
out.write(new ObjectMapper().writeValueAsString(frontResult));
out.flush();
out.close();
})
.and()
.authorizeRequests()
.anyRequest().authenticated()// 必須認證之后才能訪問
.and()
.formLogin()// 表單登錄
.permitAll() // 和表單登錄相關的接口統統都直接通過
.and()
.logout().deleteCookies("JSESSIONID")// 注銷登錄,刪除cookie
// 自定義注銷成功,返回json
.logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
FrontResult frontResult = FrontResult.init(FrontResult.SUCCEED, "注銷成功!");
out.write(new ObjectMapper().writeValueAsString(frontResult));
out.flush();
out.close();
}
})
.and()
// session 超時返回json提示
.sessionManagement()
.maximumSessions(5).maxSessionsPreventsLogin(true)// 同一用戶最大同時在線數量5個,超出后阻止登錄
// session 超時返回json提示
.expiredSessionStrategy(new SessionInformationExpiredStrategy() {
@Override
public void onExpiredSessionDetected(
SessionInformationExpiredEvent sessionInformationExpiredEvent) throws IOException, ServletException {
HttpServletResponse resp = sessionInformationExpiredEvent.getResponse();
// 返回提示
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
FrontResult frontResult = FrontResult.init(FrontResult.LOGIN, "登錄超時!");
out.write(new ObjectMapper().writeValueAsString(frontResult));
out.flush();
out.close();
}
});
//用重寫的Filter替換掉原有的UsernamePasswordAuthenticationFilter
http.addFilterAt(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);}
//注冊自定義的UsernamePasswordAuthenticationFilter,使用json格式數據登錄
@Bean
CustomAuthenticationFilter customAuthenticationFilter() throws Exception {
CustomAuthenticationFilter filter = new CustomAuthenticationFilter();
// 自定義登錄成功或失敗 返回json提示
filter.setAuthenticationSuccessHandler((req, resp, authentication) -> {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
FrontResult frontResult = FrontResult.init(FrontResult.SUCCEED, "登錄成功!");
out.write(new ObjectMapper().writeValueAsString(frontResult));
out.flush();
out.close();
});
filter.setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
String errorMessage = "登錄失敗";
FrontResult frontResult = FrontResult.init(FrontResult.FAILED, errorMessage);
out.write(new ObjectMapper().writeValueAsString(frontResult));
out.flush();
out.close();
}
});
filter.setFilterProcessesUrl("/user/login");
//重用WebSecurityConfigurerAdapter配置的AuthenticationManager,不然要自己組裝AuthenticationManager
filter.setAuthenticationManager(authenticationManagerBean());
return filter;
}
}
三、實現Json登錄的處理邏輯
security默認提供了Basic和表單兩種登錄方式,不支持Json格式的數據,需要對處理登錄的過濾器進行修改。這里,我們重寫了UsernamePasswordAuthenticationFilter的attemptAuthentication方法。
package com.hanstrovsky.filter;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
/**
* 自定義過濾器,重寫 attemptAuthentication方法,實現使用json格式的數據進行登錄
*
* @author Hanstrovsky
*/
@Slf4j
public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE)
|| request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) {
ObjectMapper mapper = new ObjectMapper();
UsernamePasswordAuthenticationToken authRequest = null;
try (InputStream is = request.getInputStream()) {
Map<String, String> authenticationBean = mapper.readValue(is, Map.class);
String username = authenticationBean.get("username");
String password = authenticationBean.get("password");
authRequest = new UsernamePasswordAuthenticationToken(
username, password);
} catch (IOException e) {
e.printStackTrace();
authRequest = new UsernamePasswordAuthenticationToken(
"", "");
}
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
} else {
// 保留原來的方法
return super.attemptAuthentication(request, response);
}
}
}
四、實現UserDetailsService接口
這個接口是用來提供用戶名和密碼的,可以通過查詢數據庫獲取用戶。本例直接在代碼中寫死。
package com.hanstrovsky.service;
import com.hanstrovsky.entity.MyUserDetails;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Repository;
/**
* @author Hanstrovsky
*/
@Repository
public class MyUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(final String username) throws UsernameNotFoundException {
// 可以在此處自定義從數據庫查詢用戶
MyUserDetails myUserDetail = new MyUserDetails();
myUserDetail.setUsername(username);
myUserDetail.setPassword("123456");
return myUserDetail;
}
}
五、實現PasswordEncoder接口
自定義密碼的加密方式。
package com.hanstrovsky.util;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
/**
* 自定義的密碼加密方法,實現了PasswordEncoder接口
*
* @author Hanstrovsky
*/
@Component
public class MyPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence charSequence) {
//加密方法可以根據自己的需要修改
return charSequence.toString();
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return encode(charSequence).equals(s);
}
}
六、實現UserDetails接口
這個類是用來存儲登錄成功后的用戶數據,security提供了直接獲取用戶信息的接口
package com.hanstrovsky.entity;
import lombok.Getter;
import lombok.Setter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.util.Collection;
/**
* 實現UserDetails,可自定義添加更多屬性
*
* @author Hanstrovsky
*/
@Getter
@Setter
@Component
public class MyUserDetails implements UserDetails {
//登錄用戶名
private String username;
//登錄密碼
private String password;
private Collection<? extends GrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
private boolean accountNonExpired = true;
private boolean accountNonLocked = true;
private boolean credentialsNonExpired = true;
private boolean enabled = true;
}
以上,便可以實現前后端分離項目基本的登錄功能。