Java实现自定义认证功能(基于session)
1.自定义认证逻辑
不破坏原有的过滤器链,又实现了自定义认证功能(基于Session,不是JSON交互)
- (1)验证码生成工具
package com.oldbai.Util; import com.google.code.kaptcha.Producer; import com.google.code.kaptcha.impl.DefaultKaptcha; import com.google.code.kaptcha.util.Config; import org.springframework.context.annotation.Bean; import javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.OutputStream; import java.util.Properties; import java.util.Random; /**
* 生成验证码的工具类
*/ public class VerifyCode { private int width = 100;// 生成验证码图片的宽度 private int height = 50;// 生成验证码图片的高度 private String[] fontNames = { "宋体", "楷体", "隶书", "微软雅黑" }; private Color bgColor = new Color(255, 255, 255);// 定义验证码图片的背景颜色为白色 private Random random = new Random(); private String codes = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; private String text;// 记录随机字符串 /**
* 获取一个随意颜色
*
* @return
*/ private Color randomColor() { int red = random.nextInt(150); int green = random.nextInt(150); int blue = random.nextInt(150); return new Color(red, green, blue); } /**
* 获取一个随机字体
*
* @return
*/ private Font randomFont() { String name = fontNames[random.nextInt(fontNames.length)]; int style = random.nextInt(4); int size = random.nextInt(5) + 24; return new Font(name, style, size); } /**
* 获取一个随机字符
*
* @return
*/ private char randomChar() { return codes.charAt(random.nextInt(codes.length())); } /**
* 创建一个空白的BufferedImage对象
*
* @return
*/ private BufferedImage createImage() { BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics2D g2 = (Graphics2D) image.getGraphics(); g2.setColor(bgColor);// 设置验证码图片的背景颜色 g2.fillRect(0, 0, width, height); return image; } public BufferedImage getImage() { BufferedImage image = createImage(); Graphics2D g2 = (Graphics2D) image.getGraphics(); StringBuffer sb = new StringBuffer(); for (int i = 0; i < 4; i++) { String s = randomChar() + ""; sb.append(s); g2.setColor(randomColor()); g2.setFont(randomFont()); float x = i * width * 1.0f / 4; g2.drawString(s, x, height - 15); } this.text = sb.toString(); drawLine(image); return image; } /**
* 绘制干扰线
*
* @param image
*/ private void drawLine(BufferedImage image) { Graphics2D g2 = (Graphics2D) image.getGraphics(); int num = 5; for (int i = 0; i < num; i++) { int x1 = random.nextInt(width); int y1 = random.nextInt(height); int x2 = random.nextInt(width); int y2 = random.nextInt(height); g2.setColor(randomColor()); g2.setStroke(new BasicStroke(1.5f)); g2.drawLine(x1, y1, x2, y2); } } public String getText() { return text; } public static void output(BufferedImage image, OutputStream out) throws IOException { ImageIO.write(image, "JPEG", out); } /**
* 提供一个实体类,使用网上一个现成的验证码库 kaptcha
* @return
*/ @Bean Producer verifyCode(){ Properties properties = new Properties(); properties.setProperty("kaptcha.image.width", "150"); properties.setProperty("kaptcha.image.height", "50"); properties.setProperty("kaptcha.textproducer.char.string", "0123456789"); properties.setProperty("kaptcha.textproducer.char.length", "4"); Config config = new Config(properties); DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); defaultKaptcha.setConfig(config); return defaultKaptcha; } }
- (2)验证码获取接口
package com.oldbai.controller; import com.oldbai.Util.VerifyCode; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import javax.imageio.ImageIO; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.awt.image.BufferedImage; import java.io.FileOutputStream; import java.io.IOException; @RestController public class LoginController { //提供生成图片的接口 @GetMapping("/verifyCode") public void verifyCode(HttpSession session, HttpServletResponse response) throws IOException { VerifyCode code = new VerifyCode(); BufferedImage image = code.getImage(); // 检查是否生成图片 ImageIO.write(image,"JPEG",new FileOutputStream("F:/a.jpg")); String text = code.getText(); session.setAttribute("verify_code",text); VerifyCode.output(image,response.getOutputStream()); } }
- (3)在过滤器中进行验证码校验
package com.oldbai.config; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; public class MyAuthenticationProvider extends DaoAuthenticationProvider { /**
* 目的是在于,验证验证码,只需要在登陆请求中验证即可。
* 之前的过滤器没问题,只是这个是更加高级的玩法
* 这样既不破坏原有的过滤器链,又实现了自定义认证功能。
* <p>
* 首先获取当前请求,注意这种获取方式,在基于 Spring 的 web 项目中,我们可以随时随地获取到当前请求,获取方式就是我上面给出的代码。
* 从当前请求中拿到 code 参数,也就是用户传来的验证码。
* 从 session 中获取生成的验证码字符串。
* 两者进行比较,如果验证码输入错误,则直接抛出异常。
* 最后通过 super 调用父类方法,也就是 DaoAuthenticationProvider 的 additionalAuthenticationChecks 方法,该方法中主要做密码的校验。
* </p>
*/ @Override protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { HttpServletRequest req = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); String code = req.getParameter("code"); String verify_code = (String) req.getSession().getAttribute("verify_code"); if (code == null || verify_code == null || !code.toLowerCase().equals(verify_code.toLowerCase())) { throw new AuthenticationServiceException("验证码错误"); } super.additionalAuthenticationChecks(userDetails, authentication); } }
- (4)在SecurityConfig中进行配置过滤器(因为我是使用MP-Security数据库认证,所以可以这样直接简单配制)
/**
* <p>
* 所有的 AuthenticationProvider 都是放在 ProviderManager 中统一管理的,
* 所以接下来我们就要自己提供 ProviderManager,
* 然后注入自定义的 MyAuthenticationProvider
* </p>
* <P>
* 我们需要提供一个 MyAuthenticationProvider 的实例,
* 创建该实例时,需要提供 UserDetailService 和 PasswordEncoder 实例。
* </P>
* <p>
* 通过重写 authenticationManager 方法来提供一个自己的 AuthenticationManager,
* 实际上就是 ProviderManager,
* 在创建 ProviderManager 时,加入自己的 myAuthenticationProvider。
* </p>
*/ @Bean MyAuthenticationProvider myAuthenticationProvider(){ MyAuthenticationProvider myAuthenticationProvider = new MyAuthenticationProvider(); myAuthenticationProvider.setPasswordEncoder(new BCryptPasswordEncoder()); myAuthenticationProvider.setUserDetailsService(userService); return myAuthenticationProvider; } @Override @Bean protected AuthenticationManager authenticationManager() throws Exception { ProviderManager manager = new ProviderManager(Arrays.asList(myAuthenticationProvider())); return manager; }
-
(5)实验截图
2.让 Spring Security 中的资源可以匿名访问
- SecurityConfig 中添加配制
/**
* 解决Spring Security 登录成功后总是获取不到登录用户信息
* <p>
* 在不同线程中,不能获取同一个用户登陆信息
* </p>
* <p>
* 这是不走过滤器的解决方法
* 让 Spring Security 中的资源可以匿名访问
* 不走 Spring Security 过滤器链
* 登陆接口如果放在这里,登录请求将不走 SecurityContextPersistenceFilter 过滤器,
* 也就意味着不会将登录用户信息存入 session,进而导致后续请求无法获取到登录用户信息。
* 下面是放行静态资源:/css/**、/js/**、/index.html、/img/**、/fonts/**、/favicon.ico
* 放行接口:/verifyCode
* 不能把登陆接口放这
* </p>
*/ @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/css/**","/js/**","/index.html","/img/**","/fonts/**","/favicon.ico","/verifyCode"); }
3. 保存关于 Http 请求的更多信息(变相的验证码验证)
当然,WebAuthenticationDetails 也可以自己定制,因为默认它只提供了 IP 和 sessionid 两个信息,如果我们想保存关于 Http 请求的更多信息,就可以通过自定义 WebAuthenticationDetails 来实现。
如果我们要定制 WebAuthenticationDetails,还要连同 WebAuthenticationDetailsSource 一起重新定义。
里面主要保存 SessionId 和 用户IP地址,也可以自定义保存其他东西。
- (1)MyWebAuthenticationDetails
package com.oldbai.config; import org.springframework.security.web.authentication.WebAuthenticationDetails; import javax.servlet.http.HttpServletRequest; public class MyWebAuthenticationDetails extends WebAuthenticationDetails { /**
* 用来保存是否验证正确
*/ private boolean isPassed; private String v_code; /**
* <p>
* 如果我们想扩展属性,只需要在 MyWebAuthenticationDetails 中再去定义更多属性,
* 然后从 HttpServletRequest 中提取出来设置给对应的属性即可,
* 这样,在登录成功后就可以随时随地获取这些属性了。
* </p>
*/ public MyWebAuthenticationDetails(HttpServletRequest request) { super(request); String code = request.getParameter("code"); this.v_code = code; String verify_code = (String) request.getSession().getAttribute("verify_code"); if (code != null && verify_code != null && code.toLowerCase().equals(verify_code.toLowerCase())) { isPassed = true; } } public boolean isPassed(){ return isPassed; } public String getV_code() { return v_code; } public void setV_code(String v_code) { this.v_code = v_code; } }
- (2)MyWebAuthenticationDetailsSource
@Component public class MyWebAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest,MyWebAuthenticationDetails> { @Override public MyWebAuthenticationDetails buildDetails(HttpServletRequest context) { return new MyWebAuthenticationDetails(context); } }
- (3)MyAuthenticationProvider(用上面1的代码进行改写)
public class MyAuthenticationProvider extends DaoAuthenticationProvider { @Override protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { MyWebAuthenticationDetails details = (MyWebAuthenticationDetails) authentication.getDetails(); if (!details.isPassed()) { throw new AuthenticationServiceException("验证码错误"); } super.additionalAuthenticationChecks(userDetails, authentication); } }
- (4)SecurityConfig(添加配制)
/**
* 自定义的myWebAuthenticationDetailsSource替换系统默认的WebAuthenticationDetailsSource
*/ @Autowired MyWebAuthenticationDetailsSource myWebAuthenticationDetailsSource; @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() .xxx .authenticationDetailsSource(myWebAuthenticationDetailsSource) .xxx }
- (5)测试接口
/**
* 测试接口
* @return
*/ @GetMapping("/hello") public MyWebAuthenticationDetails HelloWorld() { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); MyWebAuthenticationDetails details = (MyWebAuthenticationDetails) auth.getDetails(); return details; }
- (6)测试结果
4.踢掉上一个登陆用户
(1)基于用户内存的方法
- 直接在 SecurityConfig 中配制
@Bean HttpSessionEventPublisher httpSessionEventPublisher(){ return new HttpSessionEventPublisher(); } @Override protected void configure(HttpSecurity http) throws Exception { /**
* <p>
* 想要用新的登录踢掉旧的登录,我们只需要将最大会话数设置为 1 即可
* </p>
* <p>
* 设置session会话最大会话数为 1
* </p>
* <p>
* 禁止新的登陆操作
* </p>
*
*/ http.sessionManagement() .maximumSessions(1) .maxSessionsPreventsLogin(true); }
(2)前后端分离,使用数据库认证
- 直接在User类里面添加这两个,重写方法
@Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; User user = (User) o; return Objects.equals(username, user.username); } @Override public int hashCode() { return Objects.hash(username); }
(3)前后端分离,JSON交互
- ①SecurityConfig进行配置
这里我们要自己提供 SessionAuthenticationStrategy,
而前面处理 session 并发的是 ConcurrentSessionControlAuthenticationStrategy,
也就是说,我们需要自己提供一个 ConcurrentSessionControlAuthenticationStrategy 的实例,
然后配置给 LoginFilter,
但是在创建 ConcurrentSessionControlAuthenticationStrategy 实例的过程中,
还需要有一个 SessionRegistryImpl 对象
@Bean SessionRegistryImpl sessionRegistry() { return new SessionRegistryImpl(); }
- ②在 SecurityConfig 中的 LoginFilter 中配置 SessionAuthenticationStrategy
在这里自己手动构建 ConcurrentSessionControlAuthenticationStrategy 实例,构建时传递 SessionRegistryImpl 参数,然后设置 session 的并发数为 1,最后再将 sessionStrategy 配置给 LoginFilter。
/**
*手动构建 ConcurrentSessionControlAuthenticationStrategy 实例,构建时传递 *SessionRegistryImpl 参数,然后设置 session 的并发数为 1,最后再将 sessionStrategy 配置给 *LoginFilter
*/ @Bean LoginFilter loginFilter() throws Exception { LoginFilter loginFilter = new LoginFilter(); loginFilter.setAuthenticationSuccessHandler((request, response, authentication) -> { //... } ); loginFilter.setAuthenticationFailureHandler((request, response, exception) -> { //... } ); loginFilter.setAuthenticationManager(authenticationManagerBean()); loginFilter.setFilterProcessesUrl("/doLogin"); ConcurrentSessionControlAuthenticationStrategy sessionStrategy = new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry()); sessionStrategy.setMaximumSessions(1); loginFilter.setSessionAuthenticationStrategy(sessionStrategy); return loginFilter; }
- ③在SecurityConfig 中的 http 的config 中添加配制
重新创建一个 ConcurrentSessionFilter 的实例,代替系统默认的即可。
在创建新的 ConcurrentSessionFilter 实例时,需要两个参数:
sessionRegistry 就是我们前面提供的 SessionRegistryImpl 实例。
第二个参数,是一个处理 session 过期后的回调函数,也就是说,当用户被另外一个登录踢下线之后,你要给什么样的下线提示,就在这里来完成。
@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() ... http.addFilterAt(new ConcurrentSessionFilter(sessionRegistry(), event -> { HttpServletResponse resp = event.getResponse(); resp.setContentType("application/json;charset=utf-8"); resp.setStatus(401); PrintWriter out = resp.getWriter(); out.write(new ObjectMapper().writeValueAsString(RespBean.error("您已在另一台设备登录,本次登录已下线!"))); out.flush(); out.close(); }), ConcurrentSessionFilter.class); http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class); }
- ④手动向 SessionRegistryImpl 中添加一条记录
手动调用 sessionRegistry.registerNewSession 方法,向 SessionRegistryImpl 中添加一条 session 记录。
public class LoginFilter extends UsernamePasswordAuthenticationFilter { @Autowired SessionRegistry sessionRegistry; @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { //省略 UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); setDetails(request, authRequest); User principal = new User(); principal.setUsername(username); sessionRegistry.registerNewSession(request.getSession(true).getId(), principal); return this.getAuthenticationManager().authenticate(authRequest); } ... ... } }
- ⑤使用JSON交互的登陆,比基于数据库需要多几个步骤是:
1.配制一个SessionRegistryImpl
2.在 SecurityConfig 中的 LoginFilter 中配置 SessionAuthenticationStrategy
3.在SecurityConfig 中的 http 的config 中添加配制
4.手动向 SessionRegistryImpl 中添加一条记录
推荐有项目进行测试,反正我在postman测试没成功
5.跨域配制
- SecurityConfig
@Override protected void configure(HttpSecurity http) throws Exception { /**
* <p>
* 开启跨域
* </p>
*/ http.cors().configurationSource(corsConfigurationSource()); } /**
* <p>
* 开启跨域
* </p>
* <p>
* 通过 CorsConfigurationSource 实例对跨域信息作出详细配置,
* 例如允许的请求来源、
* 允许的请求方法、
* 允许通过的请求头、
* 探测请求的有效期、
* 需要处理的路径
* 等等。
* </p>
*/ @Bean CorsConfigurationSource corsConfigurationSource() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowCredentials(true); configuration.setAllowedOrigins(Arrays.asList("*")); configuration.setAllowedMethods(Arrays.asList("*")); configuration.setAllowedHeaders(Arrays.asList("*")); configuration.setMaxAge(Duration.ofHours(1)); source.registerCorsConfiguration("/**", configuration); return source; }
6.csrf 攻击如何防御
/**
* 前后端分离中
* 不是将 _csrf 放在 Model 中返回前端了,
* 而是放在 Cookie 中返回前端
* <p>
* 前端需要从cookie 中的'XSRF-TOKEN' 提取 _csrf 的值交给后端
* 通过一个 POST 请求执行操作,注意携带上 _csrf 参数
* </p>
*/ http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
7.更新当前用户信息
/**
* 先获取当前用户信息
*/ @GetMapping("/hr/info") public User getCurrentHr(Authentication authentication) { return ((User) authentication.getPrincipal()); } /**
* 更新最新当前用户信息
*
*/ @PostMapping("/hr/info") public String updata(@RequestBody User user, Authentication authentication) { if (userService.saveOrUpdate(user)){ SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(user,authentication.getCredentials(),authentication.getAuthorities())); return "更新成功"; } return "更新失败"; }
8.防止会话固定攻击
默认的 migrateSession ,在用户匿名访问的时候是一个 sessionid,当用户成功登录之后,又是另外一个 sessionid,这样就可以有效避免会话固定攻击。
- migrateSession 表示在登录成功之后,创建一个新的会话,然后把旧的 session 中的信息复制到新的 session 中,「默认即此」
http.sessionManagement().sessionFixation().migrateSession();
- none 表示不做任何事情,继续使用旧的 session。
http.sessionManagement().sessionFixation().none ();
- changeSessionId 表示 session 不变,但是会修改 sessionid,这实际上用到了 Servlet 容器提供的防御会话固定攻击。
http.sessionManagement().sessionFixation().changeSessionId ();
- newSession 表示登录后创建一个新的 session。
http.sessionManagement().sessionFixation().newSession ();
写在最后
- 以上这些,对于个人的练习开放小项目,足够了。
- 个人思路构建一个小型项目逻辑
1.创建工程,导入MyBatis-Plus 依赖,进行配置,单元测试是否连接成功
2.导入Security 依赖,进行配置
3.使用MP与Security进行整合,基于数据库的认证。
4.进行角色等级配置
5.设置最大会话数,也就是踢掉登陆或者不让登陆
6.配置登陆成功、失败、无状态访问、注销回调
7.配置验证码生成工具,开放验证码接口
8.自定义一个登陆逻辑过滤器用于验证验证码是否正确
9.如果是前后端分离项目,进行跨域配制
10.防止固定会话、开启csrf 防御,前端记得要从cookid中拿到并携带 _csrf 的参数进行请求
11.更新用户信息的配制,主要是在接口中进行配置
12.修改密码的配制,还没会,所以先不急。
本文地址:https://blog.csdn.net/qq_45031575/article/details/107876749