概述
对于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());
}
}
最后
以上就是踏实往事为你收集整理的SpringSecurity<4>的全部内容,希望文章能够帮你解决SpringSecurity<4>所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
发表评论 取消回复