对于Springsecuriy 中
呢么我们该如何给我们的web前后端不分离
或者前后端分离加上验证码验证呢~~~~
对于传统web中
//
我们的验证码
是通过res相应流响应的~ 我们先加入 谷歌的 kaptcha 这个 jar 用于 图片的生成
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
我们第一步需要 生成图片 存入session, 当用户输入 的时候进行比对~~
网上找一个 KaptchaConfig 进行配置
传统web 登录验证码~~~~~~~ 我此时加入的是 内存数据源~
/*
* 自定义springsecurity的 相关配置
*
* */
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/* 自定义数据源*/
@Bean
public UserDetailsService userDetailsService() {
// 返回内存的userDetailService
InMemoryUserDetailsManager userDetailsService =
new InMemoryUserDetailsManager();
userDetailsService.createUser
(User.withUsername("user")
.password("{noop}123").roles("admin").build());
// /*我们就吧内存的用户信息进行覆盖*/
return userDetailsService;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 设置数据源
auth.userDetailsService(userDetailsService());//
}
/* http 用来去控制http 请求的*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.
authorizeRequests() // 开启认证 所有的Http请求开启认证
// login 页面 就放行
.mvcMatchers("/login.html").permitAll() // 当时login.html 放行
/*验证码的请求 必须的放行*/
.mvcMatchers("/vc.jpg").permitAll() // 当时login.html 放行
.anyRequest() // 任何请求都要求认证
.authenticated() // 所有请求都的认证
.and()
.formLogin() // 表单认证
.loginPage("/login.html")// 指定自定义登录页面
/*拦截doLogin 请求*/
// .loginProcessingUrl("/doLogin") // 请求url
// .usernameParameter("uname")
// .passwordParameter("passwd") -------->>>> 替换为 KapterFilter 因为我们现在 用的不再是UsernamePasswordAuthenticationFilter 用UsernamePasswordAuthenticationFilter 有默认的实现
// .defaultSuccessUrl("/index.html", true)/*重定向处理*/
// .failureUrl("/login.html") /*重定向到我我们的登录页面 吧信息存储在session 作用域中*/
.and()
.logout()/*开启退出登录*/
/**/
.logoutUrl("/logout")/*路径 get 请求*/
.logoutSuccessUrl("/login.html")/* 退出之后回到login.html*/
/* 退出成功的页面*/
.and()
.csrf()
.disable();/* csrf漏洞保护给关闭了*/
//
// .failureUrl("/login.html")//重定向到登录页面
// .and()
// .logout()//开启退出登录
// .logoutUrl("/logout")
// .logoutSuccessUrl("/login.html")
// .and()
// .csrf().disable();//csrf 关闭
// 验证码的Filter 替换UsernamePasswordAuthenticationFilter
// 替换 UsernamePasswordAuthenticationFilter
// 这样以后在form表单认证的时候 首先进入kapterFilter 所在的过滤器
// 然后在
http.addFilterAt(kapterFilter(), UsernamePasswordAuthenticationFilter.class);
}
/*现在的认证交给KapteFilter来处理了
*
* 1. 认证的url
*
*
* */
@Bean
public KapterFilter kapterFilter() throws Exception {
// 自定义Filter 替换掉 UsernamePasswordAuthenticationFilter
/*
loginProcessingUrl
usernameParameter
passwordParameter
*
* */
KapterFilter kapterFilter = new KapterFilter();
kapterFilter.setFilterProcessesUrl("/doLogin");
kapterFilter.setUsernameParameter("uname");
kapterFilter.setPasswordParameter("passwd");
// set 方法注入 有的就用有的 没有用默认的
kapterFilter.setKapttchaParameter("kaptcha");
/* 指定认证管理器 */
kapterFilter.setAuthenticationManager( authenticationManagerBean() );
/*认证成功的处理 -->defaultSuccessUrl 自定义了 覆盖他 */
/* 我认证通过我还是希望 重定向到 index.html*/
// 认证成功 认证失败只是简单的做一个页面的跳转以及响应 kapterFilter.setAuthenticationSuccessHandler((req,resp,auth)->{
/* 成功时候的处理*/
resp.sendRedirect("/index.html");
});
/*认证失败的处理---??覆盖他 failureUrl */
kapterFilter.setAuthenticationFailureHandler((req,resp,ex)->{
resp.sendRedirect("/login.html");
});
return kapterFilter;
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
我此时 开发一个vc.vc.jpg 的接口 并且通过response响应出去 ~ 再之后加入到springsecurity 的配置中 标记 可以放行通过~
@Controller
public class VerifyCodeController {
@Autowired
Producer producer;
@RequestMapping("vc.jpg")
public void verifyCode(HttpServletResponse response, HttpSession session) throws IOException {
// 生成验证码
String text = producer.createText();
// 保存到session中
session.setAttribute("kaptcha", text);
//3. 生成图片
BufferedImage b1 = producer.createImage(text);
// 设置响应图片
response.setContentType("image/png");
//4. 响应图片
ServletOutputStream os = response.getOutputStream();
ImageIO.write(b1, "jpg", os);
}
}
/*自定义验证码的Filter*/
public class KapterFilter extends UsernamePasswordAuthenticationFilter {
public static final String FORM_KAPTCHA_KEY = "kaptcha";
/*可配置 这个就相当于我们定义一个默认的*/
public String kapttchaParameter = FORM_KAPTCHA_KEY;
public String getKapttchaParameter() {
return kapttchaParameter;
}
public void setKapttchaParameter(String kapttchaParameter) {
this.kapttchaParameter = kapttchaParameter;
}
/* 先调用子类 再调用父类*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
// 从请求中获取验证码 如果你没有赋值 拿的就是默认值
// 如果赋值拿的就不是默认值 通过set 方法赋值了 就是拿的是set方法赋值之后的
String kaptcha = request.getParameter(getKapttchaParameter());
// 与session中code 取出来 kaptcha
String sessionCode = (String) request.getSession().getAttribute("kaptcha");
if (!ObjectUtils.isEmpty(sessionCode)
&& !ObjectUtils.isEmpty(kaptcha)
&& sessionCode.equals(kaptcha)
) {
// 放行请求去掉父类的认证 调用父类的 完成账号密码登录
return super.attemptAuthentication(request, response);
}
///
throw new KaptchaException("验证码不匹配!");
}
}
只是在传统web 开发中引入
验证码: <input name="kaptcha" type="text"> <img th:src="@{/vc.jpg}" alt=""> <br>
这个验证码的选择框
--------------------------------------------------------------------------------------------
呢么前后端分离的时候如何加入验证码~校验,同样我们需要引入pom 然后生成验证码响应到前端 但是前后端分离的时候 就不是用流来响应了 ,因为此时是2个系统了, 的要用base64 相映成json 给前端 ,因为前后端分离此时用的是json做数据的交互
@RestController
public class VerifyCodeController {
@Autowired
Producer producer;
@GetMapping("vc.jpg")
public String verifyCode(HttpSession session) throws IOException {
// 生成验证码
String text = producer.createText();
// 保存到session中
session.setAttribute("kaptcha", text);
//3. 生成图片
BufferedImage b1 = producer.createImage(text);
// 吧图片转成byte数组 吧图片转成内存中的byte数组
FastByteArrayOutputStream fos = new FastByteArrayOutputStream();
ImageIO.write(b1, "jpg", fos);
// 输出到fos 中 吧 b1--->以jpg 输出到fos中 内存中的以字节的方式
// 4. 返回base64_____>>> 转为base64
String s = Base64Utils.encodeToString(fos.toByteArray());
return s;
}
}
自定义LoginKapterFilter
/*自定义Filter*/
public class LoginKapterFilter extends UsernamePasswordAuthenticationFilter {
public static final String FORM_KAPTCHA_KEY = "kaptcha";
/*可配置 这个就相当于我们定义一个默认的*/
public String kapttchaParameter = FORM_KAPTCHA_KEY;
public String getKapttchaParameter() {
return kapttchaParameter;
}
public void setKapttchaParameter(String kapttchaParameter) {
this.kapttchaParameter = kapttchaParameter;
}
/* 先调用子类 再调用父类 覆盖视图认证的方法
*
* 如果验证码认证通过之后 再调用我的manager
*
* */
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
//1. 获取请求验证码
Map<String, String> userInfo = new HashMap<>();
try {
userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
} catch (IOException e) {
e.printStackTrace();
}
// 设置一个默认值 get/set方法 可以改变
String kaptcha = userInfo.get(getKapttchaParameter()); // 获取数据中的验证码
String username = userInfo.get(getUsernameParameter()); // 获取数据中的账号
String passwd = userInfo.get(getPasswordParameter()); // 获取数据中的密码
//2. 获取session中验证码
String sessionVerifyCode = (String) request.getSession().getAttribute("kaptcha");
//3.获取用户名和密码认证
if (!ObjectUtils.isEmpty(sessionVerifyCode)
&& !ObjectUtils.isEmpty(kaptcha)
&& kaptcha.equals(sessionVerifyCode)) {
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, passwd);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
throw new KaptchaException("验证码异常 !!");
}
}
1. 放行 /vc.jpg 2. 使用内存型的数据源~
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
/*放行验证码*/
.mvcMatchers("/vc.jpg").permitAll()
.anyRequest()
.authenticated()
.and()
/*配置异常处理*/
.exceptionHandling()
/*当出现认证异常时 我们执行认证的 EntryPoint*/
.authenticationEntryPoint((req, response, ex) -> {
/*指定响应编码*/
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
/*未被认证的 当你没有认证的时候返回状态码 未被授权的*/
response.setStatus(HttpStatus.UNAUTHORIZED.value());
/*打印一句话*/
response.getWriter().println("请认证 ~之后再做处理");
})
/*怎么认证 formlogin 认证 [默认是不行的 进行替换 我需要自定义filter]*/
.and()
.formLogin()
.and()
.logout()
.and()
.csrf().disable()
; // 所有请求开启认证
让自定义Filter 替换 UsernamePasswordAuthenticationFilter
让我的http 请求添加 loginKapterFilter 然后替换 UsernamePasswordAuthenticationFilter
// 一旦替换以后很多特性就没有了 需要指定了
http.addFilterAt(loginKapterFilter(), UsernamePasswordAuthenticationFilter.class);
}
/*authenticationManagerBean */
/*自定义mananger 但是不能被外部使用 如果我们要暴露出来的化 我们必须要覆盖这个方法*/
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/*配置自定义LoginKapterFilter*/
@Bean
public LoginKapterFilter loginKapterFilter() throws Exception {
LoginKapterFilter loginKapterFilter = new LoginKapterFilter();
//认证的url
loginKapterFilter.setFilterProcessesUrl("/doLogin");
//认证接受的参数
loginKapterFilter.setUsernameParameter("uname");
loginKapterFilter.setPasswordParameter("passwd");
loginKapterFilter.setKapttchaParameter("kaptcha");
// 注入authenticationManager【】指定认证管理器
loginKapterFilter.setAuthenticationManager(authenticationManagerBean());
/// 成功 认证成功的处理 --------->>>> 自定义成功的处理
loginKapterFilter.setAuthenticationSuccessHandler((req, rep, authentication) -> {
HashMap<Object, Object> resMap = new HashMap<>();
resMap.put("msg", "登录成功");
// 强转成用户对象
resMap.put("userInfo", authentication.getAuthorities());
rep.setStatus(HttpStatus.OK.value());
rep.setContentType("application/json; charset=UTF-8");
String stringRsult = new ObjectMapper().writeValueAsString(resMap);
rep.getWriter().println(stringRsult);
});
/// 失败的 认证成功的处理 一般我们会改变响应的状态码
/ 自定义失败的处理
loginKapterFilter.setAuthenticationFailureHandler((req, rep, exception) -> {
HashMap<Object, Object> resultMap = new HashMap<>();
/*失败信息*/
resultMap.put("msg", "登录失败" + exception.getMessage());
rep.setContentType("application/json; charset=UTF-8");
// 改变响应码
rep.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
String stringRsult = new ObjectMapper().writeValueAsString(resultMap);
rep.getWriter().println(stringRsult);
});
return loginKapterFilter;
}
/*自定义mananger 但是不能被外部使用 如果我们要暴露出来的化 我们必须要覆盖这个方法*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService());
}
/* UserDetailsService 使用内存数据库*/
@Bean
public UserDetailsService userDetailsService() {
UserDetails serDetails = User.withUsername("user")
.password("{noop}123").roles("admin").build();
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
inMemoryUserDetailsManager.createUser(serDetails);
return inMemoryUserDetailsManager;
}
}
// 写一个自定义异常~~~~~~~~~
public class KaptchaException extends AuthenticationException {
public KaptchaException(String explanation) {
super(explanation);
}
public KaptchaException(String explanation,Throwable cause) {
super(explanation, cause);
}
}
-------------------------------------->>>>
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
Springsecurity 中的密码加密
凡是涉及密码的地方,我们都采用明文存储,在实际项目中这肯定是不可取的,因为这会带来极高的安全风险。在企业级应用中,密码不仅需要加密,还需要加`盐`,最大程度地保证密码安全。
我们一般会吧盐存储在 数据库中 因为加盐的拼接位置不同 也会增加密码的安全
我们之前只是 做账号的匹配,我们看下源码 看下密码的匹配在哪里做的
DelegatingPasswordEncoder
根据前缀选择不同的 PasswordEncoder
再之后不同的 PasswordEncoder 进行不同的处理~比对密码
默认的用的是他 也就是 noop是明文的比对
明文比对就会做equals() ..判断
默认是有这么多加密方式~
他里面的方法就在这里了
他在比对密码的时候会用你的 密码的前缀来选择不同的 不同的策略 然后再进行不一样的策略
这样就能保证我一个系统有多种策略方式 ~
允许同一个系统中存储多个不同中密码加密方案
/*passwordEncoder 指定版本号 盐 长度*/
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(16);
/* 16次散列 默认是10次*/
String encode = bCryptPasswordEncoder.encode("123");
System.out.println(encode);
/ 这个就是使用123进行加密的 而且每次加密都不一样 而且运行的时候很慢 因为他会占用系统大量资源
//
{bycrpt}$2a$16$.SAgkod45suAcQEkyLNtZeMekhZDXXXOE17XHqzFQSDjvxwPHZwNm
我们可以用一下代码生成一个密码~ 然后 我们在set密码的时候前面加一个
{bycrpt}
这样他在密码解密的时候 就会用bycrpt 这种 PasswordEncoder 进行解密了
我们也可以让整个系统用这么一种密码匹配器 这样的话 前面就不用加前缀了~
推荐使用DelegatingPasswordEncoder 的另外一个好处就是自动进行密码加密方案的升级,这个功能在整合一些老的系统时非常有用。
比如说当前数据库密码是明文{noop} 然后springsecurity 这个版本改成最新的了 之后我们需要认证后更新数据库db
在认证成功以后 ,做完全密码匹配以后 这里如果我们实现了 UserDetailsPasswordService 这个接口 会自动给我们做update 为最新密码的 此时会查询最新的策略 进行密码update
然后我root 密码转成 bcrypt 了
~~~~~~~>>>>>>>>RememberMe<----------
Remember就像 这里的复选框一样一样~,
我们这里说的Remember是服务端的一种行为 传统登录方式会基于session会话,一旦
用户的session超时过期 就需要再次登录,
比如说我此时设置的
# 默认修改服务端的会话
server.servlet.session.timeout=1 是1 分钟 默认的是,如果客户端不操作的话 默认1分钟之后session 过期就得要重新登录
RememberMe是 响应一个JsessoinId的同时还会响应一个用户的加密信息的cookie
Remember在Springsecurity中开启很容易
当我们开启RememberMe之后 这里就弹出来一个窗口
当我们点了以后会回应一个 Remember-me的 cookie 然后我们再过1分钟之后 追一下源码看看
他根据这个cookie 进行的一个[Remember]登录
我们看看RememberMeAuthenticationFilter 中的 doFilter 是如何做处理的请求
如果
SecurityContextHolder.getContext().getAuthentication() 没有值
代表session过期了~
Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);
我们需要重新去认证
当他重新认证的时候,他会解析请求中的cookie是否有 remember-me
之后将 remember-me 的值进行一个Base64解码,然后再按照: 切割成数组
// 各种校验之后 就可以通过.loadUserByUsername 再进行加载用户了
UserDetails userDetails = getUserDetailsService().loadUserByUsername(cookieTokens[0]);
String data = username + ":" + tokenExpiryTime + ":" + password + ":" + getKey();
根据这些封装一个签名 进行md5解密~ 如果解密成功就正常响应了
successfulAuthentication(request, response, chain, authenticationResult);
在这里认证成功后 如果我们勾选了~ remember的话 就会进行在resp中写remember
这样 的话他在session过期之后就会根据这个remember-me的cookie进行解密了~
但是有个问题就是 安全性问题 如果别人拿着这个remember-me的cookie 貌似也可以登录 因为此时他的cookie是不变的
所以我们可以
onLoginSuccess 生成token 回写response
processAutoLoginCookie 处理token
我们刚刚使用的 TokenBasedRememberMeServices 来做这些的 这个是最简单的所以叫做tokenBase
我们可以使用 PersistentTokenBasedRememberMeServices
这个每次 验证完token的时候会回写一个新的token
根据 token.getSeries()
但是我们上述的token 每次刷新只是在内存中用map存储的
我们如何做 才能吧这个token 存储在jdbc 中呢~~~
/*
* 自定义springsecurity的 相关配置
*
* */
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//
// /* 让所有请求都认证 开启表单认证 关闭csrf*/
// @Override
// protected void configure(HttpSecurity http) throws Exception {
// http.authorizeRequests()
// .anyRequest()
// .authenticated()
// .and()
// .formLogin()
// .and()
// /* 开启rememberMe 开启记住我的功能 有个勾选框 */
// .rememberMe()
[如果没有勾选他的时候他就会对你的请求进行一个判断了]
.alwaysRemember(true)// 前端勾选不勾选都是rememberme 的状态
.rememberMeParameter("aa")// 接受的是a 用来接受请求的参数 用来记录开启记住我的参数
//
// /*key 是什么*/
// .key(UUID.randomUUID().toString())
// .and()
// .csrf().disable();
// }
/* 让所有请求都认证 开启表单认证 关闭csrf*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.formLogin()
.and()
/* 开启rememberMe 开启记住我的功能 有个勾选框 */
.rememberMe()
.rememberMeServices(rememberMeServices()) /*指定RememberService的实现*/
.key(UUID.randomUUID().toString())
.and()
.csrf().disable();
}
@Bean
public UserDetailsService userDetailsService() {
// 返回内存的userDetailService
InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
userDetailsService.createUser
(User.withUsername("user")
/*明文123*/
.password("{noop}123").roles("admin").build());
/*我们就吧内存的用户信息进行覆盖*/
return userDetailsService;
}
// userDetailsService 做一个全局的设置
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 设置数据源
auth.userDetailsService(userDetailsService());//
}
、、、、、 @Bean标注一下使用那个RememberService 但是 只是内存的map而已
@Bean
public RememberMeServices rememberMeServices() {
// 生成令牌时候的key
String key = UUID.randomUUID().toString();//*或者 固定的Key*/
默认是基于内存的
return new PersistentTokenBasedRememberMeServices(key,userDetailsService(),new InMemoryTokenRepositoryImpl());
}
}
|