目录
- 本章内容
- 思路
- 方案
- 怎么将字符串变成图片验证码?
- kaptcha这么玩
- hutool这么玩
- 传统web项目
- 过滤器方式
- 认证器方式
- 总结下
- 前后端分离项目
- 基于过滤器方式
- 基于认证器方式
本章内容
Spring security添加图片验证方式,在互联网上面有很多这种博客,都写的非常的详细了。本篇主要讲一些添加图片验证的思路。还有前后端分离方式,图片验证要怎么去处理?
- 图片验证的思路
- 简单的demo
思路
小白: "我们从总体流程上看图片验证在认证的哪一个阶段?"
小黑: "在获取客户输入的用户名密码那一阶段,而且要在服务器获取数据库中用户名密码之前。这是一个区间[获取请求用户名密码, 获取数据库用户名密码)
而在 Spring security中, 可以很明显的发现有两种思路。
- 第1种思路是在拦截登录请求准备认证的那个过滤器。
- 第2种思路是在那个过滤器背后的认证器。"
小白: "为什么是这个阶段呢? 不能是在判断密码验证之前呢?"
小黑: "你傻啊, 如果在你说的阶段, 服务器需要去数据库中获取用户信息, 这相当的浪费系统资源"
小白: "哦哦, 我错了, 让我屡屡整个流程应该是啥样"
小白: "我需要事先在后端生成一个验证码,然后通过验证码返回一张图片给前端。前端登录表单添加图片验证。用户输入图片验证后点击登录,会存放在request请求中, 后端需要从request请求中读取到图片验证,判断前后端验证码是否相同, 如果图片验证码相同之后才开始从数据库拿用户信息。否则直接抛出认证异常"
简单点: 数据库获取用户账户之前, 先进行图片验证码验证
方案
怎么将字符串变成图片验证码?
这轮子肯定不能自己造, 有就拿来吧你
- kaptcha
- hutool
kaptcha这么玩
<!--验证码生成器--> | |
<dependency> | |
<groupId>com.github.penggle</groupId> | |
<artifactId>kaptcha</artifactId> | |
<version>.3.2</version> | |
<exclusions> | |
<exclusion> | |
<artifactId>javax.servlet-api</artifactId> | |
<groupId>javax.servlet</groupId> | |
</exclusion> | |
</exclusions> | |
</dependency> | |
public DefaultKaptcha captchaProducer() { | |
Properties properties = new Properties(); | |
properties.put("kaptcha.border", "no"); | |
properties.put("kaptcha.textproducer.char.length",""); | |
properties.put("kaptcha.image.height",""); | |
properties.put("kaptcha.image.width",""); | |
properties.put("kaptcha.obscurificator.impl","com.google.code.kaptcha.impl.ShadowGimpy"); | |
properties.put("kaptcha.textproducer.font.color","black"); | |
properties.put("kaptcha.textproducer.font.size",""); | |
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","acdefhkmnprtwxy"); | |
DefaultKaptcha kaptcha = new DefaultKaptcha(); | |
kaptcha.setConfig(new Config(properties)); | |
return kaptcha; | |
} | |
private DefaultKaptcha producer; | |
"/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这么玩
public void getHtoolVerifyCode(HttpServletResponse response, HttpSession session) throws IOException { | |
CircleCaptcha circleCaptcha = CaptchaUtil.createCircleCaptcha(, 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 的方式需要配置匹配器 | |
*/ | |
public class ValidateCodeFilter extends OncePerRequestFilter { | |
private final String login; | |
private static final AntPathRequestMatcher requiresAuthenticationRequestMatcher = new AntPathRequestMatcher(this.login, | |
"POST"); | |
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每次浏览器请求过来, 都会调用过滤器. 但是过滤器顺序是非常重要的
public class IndexController { | |
public String login() { | |
return "login"; | |
} | |
public Principal index(Principal principal) { | |
return principal; | |
} | |
} | |
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"; | |
public ValidateCodeFilter validateCodeFilter() { | |
return new ValidateCodeFilter(LOGIN_PROCESSING_URL); | |
} | |
// @Bean | |
// public WebSecurityCustomizer webSecurityCustomizer() { | |
// return web -> web.ignoring() | |
// .antMatchers("/js/**", "/css/**", "/images/**"); | |
// } | |
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"
p>小黑: "默认这个函数需要HttpSecurity调用的, 我们自定义的Filter并没有重写Configurer这个环节"
小白: "哦, 我知道了, 那我就是要继承至UsernamePasswordAuthenticationFilter呢? 我要怎么做?"
小黑: "也行, 这样就可以不用配置AntPathRequestMatcher了"
public class VerifyCodeFilter extends UsernamePasswordAuthenticationFilter { | |
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); | |
} | |
} | |
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上配置"
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 { | |
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 | |
*/ | |
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的流只能执行一次, 会报错"
小黑: "我们可以这么玩"
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还是会执行, 最后报错信息就不对了, 本来应该是验证码错误, 将会变成用户名和密码错误"
小黑: "还有就是, 很多人很喜欢在旧版本像下面这么玩"
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中执行, 他是无效的"
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上下文"
public MyDaoAuthenticationProvider authenticationProvider() throws Exception { | |
MyDaoAuthenticationProvider authenticationProvider = new MyDaoAuthenticationProvider(Constants.LOGIN_USERNAME, Constants.LOGIN_PASSWORD); | |
authenticationProvider.setPasswordEncoder(passwordEncoder()); | |
authenticationProvider.setUserDetailsService(inMemoryUserDetailsManager()); | |
return authenticationProvider; | |
} |
小白: "那还有别的思路么?"
小黑: "还有么? 不清楚了, 万能网友应该知道"
小白: "就这样设置就行了? 其他还需不需要配置?"
小黑: "其他和过滤器方式一致"
总结下
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; | |
} |
和
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 { | |
private ObjectMapper objectMapper; | |
/** | |
* 很多人这里同时支持前后端分离, 其实不对, 既然是前后端分离就彻底点 | |
* 但为了跟上潮流, 我这里也搞前后端分离 | |
* | |
* @param request | |
* @param response | |
* @return | |
* @throws AuthenticationException | |
*/ | |
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_UTF_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也行"
public class SecurityConfig { | |
private AuthenticationConfiguration authenticationConfiguration; | |
PasswordEncoder passwordEncoder() { | |
return PasswordEncoderFactories.createDelegatingPasswordEncoder(); | |
} | |
public ObjectMapper objectMapper() throws Exception { | |
return new ObjectMapper(); | |
} | |
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",); | |
map.put("msg", exception.getMessage()); | |
response.setContentType(MediaType.APPLICATION_JSON_UTF_VALUE); | |
response.getWriter().write(JSONUtil.toJsonStr(map)); | |
}); | |
verifyCodeFilter.setAuthenticationSuccessHandler((request, response, authentication) -> { | |
HashMap<String, Object> map = new HashMap<>(); | |
map.put("status",); | |
map.put("msg", "登录成功"); | |
map.put("user", authentication); | |
response.setContentType(MediaType.APPLICATION_JSON_UTF_VALUE); | |
response.getWriter().write(JSONUtil.toJsonStr(map)); | |
}); | |
return verifyCodeFilter; | |
} | |
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",); | |
map.put("msg", "注销成功"); | |
map.put("user", authentication); | |
response.setContentType(MediaType.APPLICATION_JSON_UTF_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",); | |
map.put("msg", "您没有权限, 拒绝访问: " + accessDeniedException.getMessage()); | |
// map.put("msg", "您没有权限, 拒绝访问"); | |
response.setContentType(MediaType.APPLICATION_JSON_UTF_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_UTF_VALUE); | |
response.getWriter().write(JSONUtil.toJsonStr(map)); | |
}); | |
return httpSecurity.build(); | |
} | |
} |
注意这两行代码, 教你怎么在不使用WebSecurityConfigurerAdapter的情况下拿到AuthenticationManager
public class VerifyCodeController { | |
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(); | |
} | |
} | |
public class IndexController { | |
public String login() { | |
return "login"; | |
} | |
public Principal myIndex(Principal principal) { | |
return principal; | |
} | |
} |
基于认证器方式
public class MyDaoAuthenticationProvider extends DaoAuthenticationProvider { | |
private ObjectMapper objectMapper; | |
private final String loginUsername; | |
private final String loginPassword; | |
public MyDaoAuthenticationProvider(String loginUsername, String loginPassword) { | |
this.loginUsername = loginUsername; | |
this.loginPassword = loginPassword; | |
} | |
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_UTF_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("验证码错误, 请重新获取验证码"); | |
} | |
} | |
} | |
public class SecurityConfig { | |
private static final String NOOP_PASSWORD_PREFIX = "{noop}"; | |
private static final Pattern PASSWORD_ALGORITHM_PATTERN = Pattern.compile("^\\{.+}.*$"); | |
private SecurityProperties properties; | |
PasswordEncoder passwordEncoder() { | |
return PasswordEncoderFactories.createDelegatingPasswordEncoder(); | |
} | |
public ObjectMapper objectMapper() { | |
return new ObjectMapper(); | |
} | |
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; | |
} | |
public MyDaoAuthenticationProvider authenticationProvider() throws Exception { | |
MyDaoAuthenticationProvider authenticationProvider = new MyDaoAuthenticationProvider(Constants.LOGIN_USERNAME, Constants.LOGIN_PASSWORD); | |
authenticationProvider.setPasswordEncoder(passwordEncoder()); | |
authenticationProvider.setUserDetailsService(inMemoryUserDetailsManager()); | |
return authenticationProvider; | |
} | |
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 { | |
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { | |
HashMap<String, Object> map = new HashMap<>(); | |
map.put("status",); | |
map.put("msg", "认证成功"); | |
map.put("user_info", authentication.getPrincipal()); | |
response.setContentType(MediaType.APPLICATION_JSON_UTF_VALUE); | |
response.getWriter().write(JSONUtil.toJsonStr(map)); | |
} | |
} | |
private static class MyAuthenticationFailureHandler implements AuthenticationFailureHandler { | |
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",); | |
map.put("msg", "认证失败"); | |
map.put("exception", exception.getMessage()); | |
response.setContentType(MediaType.APPLICATION_JSON_UTF_VALUE); | |
response.getWriter().write(JSONUtil.toJsonStr(map)); | |
} | |
} | |
private static class MyAuthenticationEntryPoint implements AuthenticationEntryPoint { | |
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_UTF_VALUE); | |
response.getWriter().write(JSONUtil.toJsonStr(map)); | |
} | |
} | |
private static class MyAccessDeniedHandler implements AccessDeniedHandler { | |
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { | |
log.error("没有权限", accessDeniedException); | |
HashMap<String, Object> map = new HashMap<>(); | |
map.put("status",); | |
map.put("msg", "您没有权限, 拒绝访问: " + accessDeniedException.getMessage()); | |
// map.put("msg", "您没有权限, 拒绝访问"); | |
response.setContentType(MediaType.APPLICATION_JSON_UTF_VALUE); | |
response.getWriter().write(JSONUtil.toJsonStr(map)); | |
} | |
} | |
private static class MyLogoutSuccessHandler implements LogoutSuccessHandler { | |
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { | |
HashMap<String, Object> map = new HashMap<>(); | |
map.put("status",); | |
map.put("msg", "注销成功"); | |
map.put("user", authentication); | |
response.setContentType(MediaType.APPLICATION_JSON_UTF_VALUE); | |
response.getWriter().write(JSONUtil.toJsonStr(map)); | |
} | |
} | |
} |