欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Java实现自定义认证功能(基于session)

程序员文章站 2022-04-15 17:47:03
Security的玩法是跟着江南一点雨学习的。本文是跟着江南一点雨学习的学习笔记!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.ka...

1.自定义认证逻辑

不破坏原有的过滤器链,又实现了自定义认证功能(基于Session,不是JSON交互)
Java实现自定义认证功能(基于session)

  • (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)实验截图
    Java实现自定义认证功能(基于session)

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)测试结果

Java实现自定义认证功能(基于session)

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()); 

Java实现自定义认证功能(基于session)

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

相关标签: Java