前言
Spring security添加圖片驗證方式,在互聯網上面有很多這種博客,都寫的非常的詳細了。本篇主要講一些添加圖片驗證的思路。還有前後端分離方式,圖片驗證要怎麼去處理?
本章內容
- 圖片驗證的思路
- 簡單的demo
思路
小白: "我們從總體流程上看圖片驗證在認證的哪一個階段?"
小黑: "在獲取客戶輸入的用戶名密碼那一階段,而且要在服務器獲取數據庫中用戶名密碼之前。這是一個區間[獲取請求用戶名密碼, 獲取數據庫用戶名密碼)
而在 Spring security中, 可以很明顯的發現有兩種思路。
第1種思路是在攔截登錄請求準備認證的那個過濾器。
第2種思路是在那個過濾器背後的認證器。"
小白: "爲什麼是這個階段呢? 不能是在判斷密碼驗證之前呢?"
小黑: "你傻啊, 如果在你說的階段, 服務器需要去數據庫中獲取用戶信息, 這相當的浪費系統資源"
小白: "哦哦, 我錯了, 讓我屢屢整個流程應該是啥樣"
小白: "我需要事先在後端生成一個驗證碼,然後通過驗證碼返回一張圖片給前端。前端登錄表單添加圖片驗證。用戶輸入圖片驗證後點擊登錄,會存放在request
請求中, 後端需要從request
請求中讀取到圖片驗證,判斷前後端驗證碼是否相同, 如果圖片驗證碼相同之後纔開始從數據庫拿用戶信息。否則直接拋出認證異常"
簡單點: 數據庫獲取用戶賬戶之前, 先進行圖片驗證碼驗證
方案
怎麼將字符串變成圖片驗證碼?
這輪子肯定不能自己造, 有就拿來吧你
kaptcha
hutool
kaptcha
這麼玩
<!--驗證碼生成器-->
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
<exclusions>
<exclusion>
<artifactId>javax.servlet-api</artifactId>
<groupId>javax.servlet</groupId>
</exclusion>
</exclusions>
</dependency>
@Bean
public DefaultKaptcha captchaProducer() {
Properties properties = new Properties();
properties.put("kaptcha.border", "no");
properties.put("kaptcha.textproducer.char.length","4");
properties.put("kaptcha.image.height","50");
properties.put("kaptcha.image.width","150");
properties.put("kaptcha.obscurificator.impl","com.google.code.kaptcha.impl.ShadowGimpy");
properties.put("kaptcha.textproducer.font.color","black");
properties.put("kaptcha.textproducer.font.size","40");
properties.put("kaptcha.noise.impl","com.google.code.kaptcha.impl.NoNoise");
//properties.put("kaptcha.noise.impl","com.google.code.kaptcha.impl.DefaultNoise");
properties.put("kaptcha.textproducer.char.string","acdefhkmnprtwxy2345678");
DefaultKaptcha kaptcha = new DefaultKaptcha();
kaptcha.setConfig(new Config(properties));
return kaptcha;
}
@Resource
private DefaultKaptcha producer;
@GetMapping("/verify-code")
public void getVerifyCode(HttpServletResponse response, HttpSession session) throws Exception {
response.setContentType("image/jpeg");
String text = producer.createText();
session.setAttribute("verify_code", text);
BufferedImage image = producer.createImage(text);
try (ServletOutputStream outputStream = response.getOutputStream()) {
ImageIO.write(image, "jpeg", outputStream);
}
}
hutool
這麼玩
@GetMapping("hutool-verify-code")
public void getHtoolVerifyCode(HttpServletResponse response, HttpSession session) throws IOException {
CircleCaptcha circleCaptcha = CaptchaUtil.createCircleCaptcha(200, 100, 4, 80);
session.setAttribute("hutool_verify_code", circleCaptcha.getCode());
response.setContentType(MediaType.IMAGE_PNG_VALUE);
circleCaptcha.write(response.getOutputStream());
}
這倆隨便挑選一個完事
前端就非常簡單了
<form th:action="@{/login}" method="post">
<div class="input">
<label for="name">用戶名</label>
<input type="text" name="username" id="name">
<span class="spin"></span>
</div>
<div class="input">
<label for="pass">密碼</label>
<input type="password" name="password" id="pass">
<span class="spin"></span>
</div>
<div class="input">
<label for="code">驗證碼</label>
<input type="text" name="code" id="code"><img src="/verify-code" alt="驗證碼">
<!--<input type="text" name="code" id="code"><img src="/hutool-verify-code" alt="驗證碼">-->
<span class="spin"></span>
</div>
<div class="button login">
<button type="submit">
<span>登錄</span>
<i class="fa fa-check"></i>
</button>
</div>
<div th:text="${session.SPRING_SECURITY_LAST_EXCEPTION}"></div>
</form>
傳統web項目
我們現在根據上面的思路來設計設計該怎麼實現這項功能
過濾器方式
/**
* 使用 OncePerRequestFilter 的方式需要配置匹配器
*/
@RequiredArgsConstructor
public class ValidateCodeFilter extends OncePerRequestFilter {
private final String login;
private static final AntPathRequestMatcher requiresAuthenticationRequestMatcher = new AntPathRequestMatcher(this.login,
"POST");
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (requiresAuthenticationRequestMatcher.matches(request)) {
validateCode(request);
}
filterChain.doFilter(request, response);
}
private void validateCode(HttpServletRequest request) {
HttpSession session = request.getSession();
// 獲取保存在session中的code
String verifyCode = (String) session.getAttribute("verify_code");
if (StringUtils.isBlank(verifyCode)) {
throw new ValidateCodeException("請重新申請驗證碼!");
}
// 拿到前端的 code
String code = request.getParameter("code");
if (StringUtils.isBlank(code)) {
throw new ValidateCodeException("驗證碼不能爲空!");
}
// 對比
if (!StringUtils.equalsIgnoreCase(code, verifyCode)) {
throw new AuthenticationServiceException("驗證碼錯誤!");
}
// 刪除掉 session 中的 verify_code
session.removeAttribute("verify_code");
}
}
雖然OncePerRequestFilter
每次瀏覽器請求過來, 都會調用過濾器. 但是過濾器順序是非常重要的
@Controller
@Slf4j
public class IndexController {
@GetMapping("login")
public String login() {
return "login";
}
@GetMapping("")
@ResponseBody
public Principal index(Principal principal) {
return principal;
}
}
@Configuration
public class SecurityConfig {
public static final String[] MATCHERS_URLS = {"/verify-code",
"/css/**",
"/images/**",
"/js/**",
"/hutool-verify-code"};
public static final String LOGIN_PROCESSING_URL = "/login";
public static final String LOGIN_PAGE = "/login";
public static final String SUCCESS_URL = "/index";
@Bean
public ValidateCodeFilter validateCodeFilter() {
return new ValidateCodeFilter(LOGIN_PROCESSING_URL);
}
// @Bean
// public WebSecurityCustomizer webSecurityCustomizer() {
// return web -> web.ignoring()
// .antMatchers("/js/**", "/css/**", "/images/**");
// }
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeHttpRequests()
.antMatchers(MATCHERS_URLS).permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginPage(LOGIN_PAGE)
.loginProcessingUrl(LOGIN_PROCESSING_URL)
.defaultSuccessUrl(SUCCESS_URL, true)
.permitAll()
.and()
.csrf()
.disable();
httpSecurity.addFilterBefore(validateCodeFilter(), UsernamePasswordAuthenticationFilter.class);
return httpSecurity.build();
}
}
小白: "我在網上看到有些網友並不是繼承的
OncePerRequestFilter
接口啊?"小黑: "是的, 有一部分朋友選擇繼承
UsernamePasswordAuthenticationFilter
"小黑: "繼承這個過濾器的話, 我們需要配置很多東西, 比較麻煩"
小白: "爲什麼要有多餘的配置?"
小黑: "你想想, 你自定義的過濾器繼承至
UsernamePasswordAuthenticationFilter
, 自定義的過濾器和原先的過濾器是同時存在的"小黑: "沒有爲你自定義的過濾器配置對應的
Configurer
, 那麼它裏面啥也沒有全部屬性都是默認值, 不說別的, 下面AuthenticationManager
至少要配置吧?"
小黑: "他可是沒有任何默認值, 這樣會導致下面這行代碼報錯"
小黑: "當然如果你有自定義屬於自己的
Configurer
那沒話說, 比如FormLoginConfigurer
"
小黑: "默認這個函數需要
HttpSecurity
調用的, 我們自定義的Filter
並沒有重寫Configurer
這個環節"小白: "哦, 我知道了, 那我就是要繼承至
UsernamePasswordAuthenticationFilter
呢? 我要怎麼做?"小黑: "也行, 這樣就可以不用配置
AntPathRequestMatcher
了"
public class VerifyCodeFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
HttpSession session = request.getSession();
String sessionVerifyCode = (String) session.getAttribute(Constants.VERIFY_CODE);
String verifyCode = request.getParameter(Constants.VERIFY_CODE);
if (StrUtil.isBlank(sessionVerifyCode) || StrUtil.isBlank(verifyCode)
|| !StrUtil.equalsIgnoreCase(sessionVerifyCode, verifyCode)) {
throw new ValidateCodeException("圖片驗證碼錯誤, 請重新獲取");
}
return super.attemptAuthentication(request, response);
}
}
@Bean
public VerifyCodeFilter verifyCodeFilter() throws Exception {
VerifyCodeFilter verifyCodeFilter = new VerifyCodeFilter();
verifyCodeFilter.setAuthenticationManager(authenticationConfiguration.getAuthenticationManager());
return verifyCodeFilter;
}
小黑: "這樣就可以了"
小白: "也不麻煩啊"
小黑: "好吧, 好像是"
小白: "等等, 那
SecurityFilterChain
呢? 特別是formLogin()
函數要怎麼配置?"
httpSecurity.formLogin()
.loginPage(loginPage)
.loginProcessingUrl(loginUrl)
.defaultSuccessUrl("/", true)
.permitAll();
httpSecurity.addFilterBefore(verifyCodeFilter(), UsernamePasswordAuthenticationFilter.class);
小白: "那我前端表單用戶名和密碼的
input
標籤的name
屬性變成user
和pwd
了呢? 也在上面formLogin
上配置?"小黑: "這裏就有區別了, 明顯只能在
VerifyCodeFilter Bean
上配置"
@Bean
public VerifyCodeFilter verifyCodeFilter() throws Exception {
VerifyCodeFilter verifyCodeFilter = new VerifyCodeFilter();
verifyCodeFilter.setAuthenticationManager(authenticationConfiguration.getAuthenticationManager());
verifyCodeFilter.setUsernameParameter("user");
verifyCodeFilter.setPasswordParameter("pwd");
return verifyCodeFilter;
}
小白: "我還以爲有多麻煩呢, 就這..."
小黑: "額, 主要是spring security的過濾器不能代替, 只能插入某個過濾器前後位置, 所以如果自定義過濾器就需要我們配置一些屬性"
認證器方式
小白: "認證器要怎麼實現圖片驗證呢?"
小黑: "說到認證的認證器, 一定要想到
DaoAuthenticationProvider
"小黑: "很多人在基於認證器實現圖片驗證時, 都重寫
additionalAuthenticationChecks
, 這是不對的"
小白: "那應該重寫哪個方法?"
小黑: "應該重寫下面那個函數"
小白: "等一下, 你注意到這個方法的參數了麼? 你這要怎麼從
request
中拿驗證碼?"小黑: "有別的方法, 看源碼"
public class MyDaoAuthenticationProvider extends DaoAuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
assert requestAttributes != null;
HttpServletRequest request = requestAttributes.getRequest();
String verifyCode = request.getParameter(Constants.VERIFY_CODE);
String sessionVerifyCode = (String) request.getSession().getAttribute(Constants.VERIFY_CODE);
if (StrUtil.isBlank(sessionVerifyCode) && StrUtil.isBlank(verifyCode)
&& !StrUtil.equalsIgnoreCase(sessionVerifyCode, verifyCode)) {
throw new ValidateCodeException("圖片驗證碼錯誤, 請重新獲取");
}
return super.authenticate(authentication);
}
}
小白: "哦, 我看到了, 沒想到還能這樣"
小白: "那你現在要怎麼加入到Spring Security, 讓它代替掉原本的
DaoAuthenticationProvider
呢?"小黑: "這裏有一個思路, 還記得
AuthenticationManager
的父子關係吧, 你看到父親只有一個, 你看到兒子可以有幾個?"小白: "好像是無數個, 那我是不是可以這麼寫?"
/**
* 往父類的 AuthenticationManager 裏添加 authenticationProvider
* 在源碼裏面是這樣的AuthenticationProvider authenticationProvider = getBeanOrNull(AuthenticationProvider.class);
*
* @return
* @throws Exception
*/
@Bean
public MyDaoAuthenticationProvider authenticationProvider() throws Exception {
MyDaoAuthenticationProvider authenticationProvider = new MyDaoAuthenticationProvider(Constants.LOGIN_USERNAME, Constants.LOGIN_PASSWORD);
authenticationProvider.setPasswordEncoder(passwordEncoder());
authenticationProvider.setUserDetailsService(inMemoryUserDetailsManager());
return authenticationProvider;
}
// 往子類AuthenticationManager裏面添加的 authenticationProvider
httpSecurity.authenticationProvider(authenticationProvider());
小黑: "這上面的代碼有問題,
AuthenticationManger
有父類和子類, 上面這段代碼同時往父類和子類都添加MyDaoAuthenticationProvider
, 這樣MyDaoAuthenticationProvider
會被執行兩次, 但request的流只能執行一次, 會報錯"小黑: "我們可以這麼玩"
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 代碼省略
// 代碼省略
// 代碼省略
// 代碼省略
// 往子類AuthenticationManager裏面添加的 authenticationProvider, 但不能阻止 AuthenticationManger 父類加載 DaoAuthenticationProvider
AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
// 但是這種方式可以將 parent Manager 設置爲 null, 所以是可以的
authenticationManagerBuilder.parentAuthenticationManager(null);
MyDaoAuthenticationProvider authenticationProvider = new MyDaoAuthenticationProvider(Constants.LOGIN_USERNAME, Constants.LOGIN_PASSWORD);
authenticationProvider.setPasswordEncoder(passwordEncoder());
authenticationProvider.setUserDetailsService(inMemoryUserDetailsManager());
authenticationManagerBuilder.authenticationProvider(authenticationProvider);
http.authenticationManager(authenticationManagerBuilder.build());
return http.build();
}
小黑: "
SecurityFilterChain
表示一個Filter
集合, 更直接點就是子類的AuthenticationManager
"小黑: "所以這種玩法是給子類
AuthenticationManager
添加Provider
, 但是它需要手動將parent
置爲null
, 否則父類的DaoAuthenticationProvider
還是會執行, 最後報錯信息就不對了, 本來應該是驗證碼錯誤, 將會變成用戶名和密碼錯誤"
小黑: "還有就是, 很多人很喜歡在舊版本像下面這麼玩"
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
MyDaoAuthenticationProvider authenticationProvider = new MyDaoAuthenticationProvider(Constants.LOGIN_USERNAME, Constants.LOGIN_PASSWORD);
authenticationProvider.setPasswordEncoder(passwordEncoder());
authenticationProvider.setUserDetailsService(inMemoryUserDetailsManager());
return new ProviderManager(authenticationProvider);
}
小黑: "在新版本也類似的這麼搞, 但這樣是有區別的, 下面這種方式只會加入到spring Bean上下文, 但是不會加入到Spring Security中執行, 他是無效的"
@Bean
public ProviderManager providerManager() throws Exception {
MyDaoAuthenticationProvider authenticationProvider = authenticationProvider();
return new ProviderManager(authenticationProvider);
}
小黑: "在新版本中, 使用上面那段代碼是一點用都沒有"
public MyDaoAuthenticationProvider authenticationProvider() throws Exception {
MyDaoAuthenticationProvider authenticationProvider = new MyDaoAuthenticationProvider(Constants.LOGIN_USERNAME, Constants.LOGIN_PASSWORD);
authenticationProvider.setPasswordEncoder(passwordEncoder());
authenticationProvider.setUserDetailsService(inMemoryUserDetailsManager());
return authenticationProvider;
}
// 往子類AuthenticationManager裏面添加的 authenticationProvider
httpSecurity.authenticationProvider(authenticationProvider());
小黑: "上面這樣做也是不行, 他還是會存在兩個, 一個是
MyDaoAuthenticationProvider
(子類), 另一個是DaoAuthenticationProvider
(父類)"小白: "那最好的辦法是什麼?"
小黑: "直接將
MyDaoAuthenticationProvider
添加到Spring Bean上下文"
@Bean
public MyDaoAuthenticationProvider authenticationProvider() throws Exception {
MyDaoAuthenticationProvider authenticationProvider = new MyDaoAuthenticationProvider(Constants.LOGIN_USERNAME, Constants.LOGIN_PASSWORD);
authenticationProvider.setPasswordEncoder(passwordEncoder());
authenticationProvider.setUserDetailsService(inMemoryUserDetailsManager());
return authenticationProvider;
}
小白: "那還有別的思路麼?"
小黑: "還有麼? 不清楚了, 萬能網友應該知道"
小白: "就這樣設置就行了? 其他還需不需要配置?"
小黑: "其他和過濾器方式一致"
總結下
@Bean
public MyDaoAuthenticationProvider authenticationProvider() throws Exception {
// 最好的辦法就是直接MyDaoAuthenticationProvider加入到Spring Bean裏面就行了, 其他都不要
MyDaoAuthenticationProvider authenticationProvider = new MyDaoAuthenticationProvider(Constants.LOGIN_USERNAME, Constants.LOGIN_PASSWORD);
authenticationProvider.setPasswordEncoder(passwordEncoder());
authenticationProvider.setUserDetailsService(inMemoryUserDetailsManager());
return authenticationProvider;
}
和
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 代碼省略
// 代碼省略
// 代碼省略
// 代碼省略
// 往子類AuthenticationManager裏面添加的 authenticationProvider, 但不能阻止 AuthenticationManger 父類加載 DaoAuthenticationProvider
AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
// 但是這種方式可以將 parent Manager 設置爲 null, 所以是可以的
authenticationManagerBuilder.parentAuthenticationManager(null);
MyDaoAuthenticationProvider authenticationProvider = new MyDaoAuthenticationProvider(Constants.LOGIN_USERNAME, Constants.LOGIN_PASSWORD);
authenticationProvider.setPasswordEncoder(passwordEncoder());
authenticationProvider.setUserDetailsService(inMemoryUserDetailsManager());
authenticationManagerBuilder.authenticationProvider(authenticationProvider);
http.authenticationManager(authenticationManagerBuilder.build());
return http.build();
}
都是可以的, 一個往父類的
AuthenticationManager
添加MyDaoAuthenticationProvider
, 另一個往子類添加, 設置父類爲null
前後端分離項目
小白: "前後端分離和傳統web項目的區別是什麼?"
小黑: "請求
request
和響應response
都使用JSON
傳遞數據"小白: "那我們分析源碼時只要關注
request
和response
咯, 只要發現存在request的讀, 和 response的寫通通都要重寫一邊"小黑: "是的, 其實很簡單, 無非是圖片驗證碼改用
json
讀, 認證時的讀取username
和password
也使用json
讀, 其次是出現異常需要響應response
, 也改成json
寫, 認證成功和失敗需要響應到前端也改成json
寫"小白: "哦, 那隻要分析過源碼, 就能夠完成前後端分離功能了"
小黑: "所以還講源碼麼? "
小白: "不用, 非常簡單"
基於過濾器方式
public class VerifyCodeFilter extends UsernamePasswordAuthenticationFilter {
@Resource
private ObjectMapper objectMapper;
/**
* 很多人這裏同時支持前後端分離, 其實不對, 既然是前後端分離就徹底點
* 但爲了跟上潮流, 我這裏也搞前後端分離
*
* @param request
* @param response
* @return
* @throws AuthenticationException
*/
@SneakyThrows
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!"POST".equals(request.getMethod())) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String contentType = request.getContentType();
HttpSession session = request.getSession();
if (MediaType.APPLICATION_JSON_VALUE.equals(contentType) || MediaType.APPLICATION_JSON_UTF8_VALUE.equals(contentType)) {
Map map = objectMapper.readValue(request.getInputStream(), Map.class);
imageJSONVerifyCode(session, map);
String username = (String) map.get(this.getUsernameParameter());
username = (username != null) ? username.trim() : "";
String password = (String) map.get(this.getPasswordParameter());
password = (password != null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
imageVerifyCode(request, session);
return super.attemptAuthentication(request, response);
}
private void imageJSONVerifyCode(HttpSession session, Map map) throws ValidateCodeException {
String verifyCode = (String) map.get(Constants.VERIFY_CODE);
String code = (String) session.getAttribute(Constants.VERIFY_CODE);
if (StrUtil.isBlank(verifyCode) || StrUtil.isBlank(code) || !StrUtil.equalsIgnoreCase(verifyCode, code)) {
throw new ValidateCodeException("驗證碼錯誤, 請重新獲取驗證碼");
}
}
private void imageVerifyCode(HttpServletRequest request, HttpSession session) throws ValidateCodeException {
String verifyCode = request.getParameter(Constants.VERIFY_CODE);
String code = (String) session.getAttribute(Constants.VERIFY_CODE);
if (StrUtil.isBlank(verifyCode) || StrUtil.isBlank(code) || !StrUtil.equalsIgnoreCase(verifyCode, code)) {
throw new ValidateCodeException("驗證碼錯誤, 請重新獲取驗證碼");
}
}
}
小白: "爲什麼你要寫
imageJSONVerifyCode
,imageVerifyCode
兩個函數? 寫一個不就行了?"小黑: "額, 是的, 把參數改成兩個
String verifyCode, String code
也行"
@Configuration
public class SecurityConfig {
@Resource
private AuthenticationConfiguration authenticationConfiguration;
@Bean
PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
public ObjectMapper objectMapper() throws Exception {
return new ObjectMapper();
}
@Bean
public VerifyCodeFilter verifyCodeFilter() throws Exception {
VerifyCodeFilter verifyCodeFilter = new VerifyCodeFilter();
verifyCodeFilter.setAuthenticationManager(authenticationConfiguration.getAuthenticationManager());
verifyCodeFilter.setAuthenticationFailureHandler((request, response, exception) -> {
HashMap<String, Object> map = new HashMap<>();
map.put("status", 401);
map.put("msg", exception.getMessage());
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.getWriter().write(JSONUtil.toJsonStr(map));
});
verifyCodeFilter.setAuthenticationSuccessHandler((request, response, authentication) -> {
HashMap<String, Object> map = new HashMap<>();
map.put("status", 200);
map.put("msg", "登錄成功");
map.put("user", authentication);
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.getWriter().write(JSONUtil.toJsonStr(map));
});
return verifyCodeFilter;
}
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeHttpRequests()
.antMatchers(Constants.MATCHERS_LIST)
.permitAll()
.anyRequest()
.authenticated()
;
httpSecurity.formLogin()
.loginPage(Constants.LOGIN_PAGE)
.loginProcessingUrl(Constants.LOGIN_PROCESSING_URL)
.defaultSuccessUrl(Constants.SUCCESS_URL, true)
.permitAll();
httpSecurity.logout()
.clearAuthentication(true)
.invalidateHttpSession(true)
.logoutSuccessHandler((request, response, authentication) -> {
HashMap<String, Object> map = new HashMap<>();
map.put("status", 200);
map.put("msg", "註銷成功");
map.put("user", authentication);
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.getWriter().write(JSONUtil.toJsonStr(map));
});
httpSecurity.csrf()
.disable();
httpSecurity.addFilterAt(verifyCodeFilter(), UsernamePasswordAuthenticationFilter.class);
httpSecurity.exceptionHandling()
.accessDeniedHandler((request, response, accessDeniedException) -> {
HashMap<String, Object> map = new HashMap<>();
map.put("status", 401);
map.put("msg", "您沒有權限, 拒絕訪問: " + accessDeniedException.getMessage());
// map.put("msg", "您沒有權限, 拒絕訪問");
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.getWriter().write(JSONUtil.toJsonStr(map));
})
.authenticationEntryPoint((request, response, authException) -> {
HashMap<String, Object> map = new HashMap<>();
map.put("status", HttpStatus.UNAUTHORIZED.value());
map.put("msg", "認證失敗, 請重新認證: " + authException.getMessage());
// map.put("msg", "認證失敗, 請重新認證");
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.getWriter().write(JSONUtil.toJsonStr(map));
});
return httpSecurity.build();
}
}
注意這兩行代碼, 教你怎麼在不使用WebSecurityConfigurerAdapter
的情況下拿到AuthenticationManager
@RestController
@Slf4j
public class VerifyCodeController {
@GetMapping("/verify-code")
public void getVerifyCode(HttpServletResponse response, HttpSession session) throws Exception {
GifCaptcha captcha = CaptchaUtil.createGifCaptcha(Constants.IMAGE_WIDTH, Constants.IMAGE_HEIGHT);
RandomGenerator randomGenerator = new RandomGenerator(Constants.BASE_STR, Constants.RANDOM_LENGTH);
captcha.setGenerator(randomGenerator);
captcha.createCode();
String code = captcha.getCode();
session.setAttribute(Constants.VERIFY_CODE, code);
ServletOutputStream outputStream = response.getOutputStream();
captcha.write(outputStream);
outputStream.flush();
outputStream.close();
}
}
@Controller
@Slf4j
public class IndexController {
@GetMapping("login")
public String login() {
return "login";
}
@GetMapping("")
@ResponseBody
public Principal myIndex(Principal principal) {
return principal;
}
}
基於認證器方式
public class MyDaoAuthenticationProvider extends DaoAuthenticationProvider {
@Resource
private ObjectMapper objectMapper;
private final String loginUsername;
private final String loginPassword;
public MyDaoAuthenticationProvider(String loginUsername, String loginPassword) {
this.loginUsername = loginUsername;
this.loginPassword = loginPassword;
}
@SneakyThrows
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
assert requestAttributes != null;
HttpServletRequest request = requestAttributes.getRequest();
String contentType = request.getContentType();
String verifyCode = (String) request.getSession().getAttribute(Constants.VERIFY_CODE);
if (MediaType.APPLICATION_JSON_VALUE.equals(contentType) || MediaType.APPLICATION_JSON_UTF8_VALUE.equals(contentType)) {
Map map = this.objectMapper.readValue(request.getInputStream(), Map.class);
String code = (String) map.get(Constants.VERIFY_CODE);
imageVerifyCode(verifyCode, code);
String username = (String) map.get(loginUsername);
String password = (String) map.get(loginPassword);
UsernamePasswordAuthenticationToken authenticationToken = UsernamePasswordAuthenticationToken
.unauthenticated(username, password);
return super.authenticate(authenticationToken);
}
String code = request.getParameter(Constants.VERIFY_CODE);
imageVerifyCode(verifyCode, code);
return super.authenticate(authentication);
}
private void imageVerifyCode(String verifyCode, String code) throws ValidateCodeException {
if (StrUtil.isBlank(verifyCode) || StrUtil.isBlank(code) || !StrUtil.equalsIgnoreCase(verifyCode, code)) {
throw new ValidateCodeException("驗證碼錯誤, 請重新獲取驗證碼");
}
}
}
@Slf4j
@Configuration
public class SecurityConfig {
private static final String NOOP_PASSWORD_PREFIX = "{noop}";
private static final Pattern PASSWORD_ALGORITHM_PATTERN = Pattern.compile("^\\{.+}.*$");
@Resource
private SecurityProperties properties;
@Bean
PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper();
}
@Bean
@Lazy
public InMemoryUserDetailsManager inMemoryUserDetailsManager() {
SecurityProperties.User user = properties.getUser();
List<String> roles = user.getRoles();
return new InMemoryUserDetailsManager(
User.withUsername(user.getName()).password(getOrDeducePassword(user, passwordEncoder()))
.roles(StringUtils.toStringArray(roles)).build());
}
private String getOrDeducePassword(SecurityProperties.User user, PasswordEncoder encoder) {
String password = user.getPassword();
if (user.isPasswordGenerated()) {
log.warn(String.format(
"%n%nUsing generated security password: %s%n%nThis generated password is for development use only. "
+ "Your security configuration must be updated before running your application in "
+ "production.%n",
user.getPassword()));
}
if (encoder != null || PASSWORD_ALGORITHM_PATTERN.matcher(password).matches()) {
return password;
}
return NOOP_PASSWORD_PREFIX + password;
}
@Bean
public MyDaoAuthenticationProvider authenticationProvider() throws Exception {
MyDaoAuthenticationProvider authenticationProvider = new MyDaoAuthenticationProvider(Constants.LOGIN_USERNAME, Constants.LOGIN_PASSWORD);
authenticationProvider.setPasswordEncoder(passwordEncoder());
authenticationProvider.setUserDetailsService(inMemoryUserDetailsManager());
return authenticationProvider;
}
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests()
.antMatchers(Constants.MATCHERS_LIST)
.permitAll()
.anyRequest()
.authenticated()
;
http.formLogin()
.loginPage(Constants.LOGIN_PAGE)
.loginProcessingUrl(Constants.LOGIN_PROCESSING_URL)
.successHandler(new MyAuthenticationSuccessHandler())
.failureHandler(new MyAuthenticationFailureHandler())
.permitAll();
http.logout()
.clearAuthentication(true)
.invalidateHttpSession(true)
.logoutSuccessHandler(new MyLogoutSuccessHandler());
http.csrf()
.disable();
http.exceptionHandling(exceptionHandlingConfigurer -> {
exceptionHandlingConfigurer.authenticationEntryPoint(new MyAuthenticationEntryPoint());
exceptionHandlingConfigurer.accessDeniedHandler(new MyAccessDeniedHandler());
})
;
return http.build();
}
private static class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
HashMap<String, Object> map = new HashMap<>();
map.put("status", 200);
map.put("msg", "認證成功");
map.put("user_info", authentication.getPrincipal());
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.getWriter().write(JSONUtil.toJsonStr(map));
}
}
private static class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
log.error("認證失敗", exception);
exception.printStackTrace();
HashMap<String, Object> map = new HashMap<>();
map.put("status", 401);
map.put("msg", "認證失敗");
map.put("exception", exception.getMessage());
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.getWriter().write(JSONUtil.toJsonStr(map));
}
}
private static class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
log.error("認證失效", authException);
HashMap<String, Object> map = new HashMap<>();
map.put("status", HttpStatus.UNAUTHORIZED.value());
map.put("msg", "認證失敗, 請重新認證: " + authException.getMessage());
// map.put("msg", "認證失敗, 請重新認證");
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.getWriter().write(JSONUtil.toJsonStr(map));
}
}
private static class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
log.error("沒有權限", accessDeniedException);
HashMap<String, Object> map = new HashMap<>();
map.put("status", 401);
map.put("msg", "您沒有權限, 拒絕訪問: " + accessDeniedException.getMessage());
// map.put("msg", "您沒有權限, 拒絕訪問");
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.getWriter().write(JSONUtil.toJsonStr(map));
}
}
private static class MyLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
HashMap<String, Object> map = new HashMap<>();
map.put("status", 200);
map.put("msg", "註銷成功");
map.put("user", authentication);
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.getWriter().write(JSONUtil.toJsonStr(map));
}
}
}