springBoot使用springSecurity入门详细配置,自定义登录,RBAC权限模型,数据库(Jpa+mysql)验证登录,验证码,json格式交互等(上篇)

只写如何应用,并不深入流程原理,如果想深入学习原理机制的只能失望了;

springSecurity5.0开始默认必须对密码加密,之前的是可选;

0.准备工作,创建初始化的springBoot项目,版本是2.2.1

  1. IDEA创建springBoot项目,勾选web,以来如下
    <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
  2. 创建一个controller类体提供一个访问接口
    @RestController
    @RequestMapping("/admin")
    public class SecurityController {
    
        @RequestMapping("/hello")
        public String hello() {
            return "Hello World";
        }
    
    }
    
  3. application.yml配置文件
    server:
      port: 8080
      tomcat:
        uri-encoding: utf-8
      servlet:
        context-path: /security
    启动运行访问:

一切准备就绪!!!

1.springBoot应用springSecurity只需一步!

得便于springBoot自动化配置,我们只需要在项目添加springSecurity的依赖就能将它运用在项目中:

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

再次运行你的项目访问接口你会发现:

需要登陆才能访问,如果你没有自己配置用户数据来源,springSecurity默认提供user用户,密码你可以在控制台找到

然后登陆就会自动跳转到受保护的接口:

2.security的配置,继承WebSecurityConfigurerAdapter类重写配置方法

security几乎所有配置其中配置,重写WebSecurityConfigurerAdapter的三个方法(其实是一个方法的重载,参数不一样而已)

  1. configure(AuthenticationManagerBuilder auth):配置用户数据来源,如果没有重写该方法默认提供user用户以及动态生成的密码
  2. void configure(HttpSecurity http):配置security的流程及工作组件,如处理器,过滤器,remenber-me等等
  3. configure(WebSecurity web):配置security作用的范围,保护那些接口等等

3.自定义登陆界面

  1. 写好自己的登陆界面,静态资源如css、js、html、图片等放在resources的static文件夹中即可
  2. 表单用户名必须为username(springSecurity默认,可以修改),密码必须为password(同用户名)
  3. 表单的action可以随意,但必须与springSecurity配置类配置登陆处理请求的url一致
  • 配置springSecurity的配置类,继承重写WebSecurityConfigurerAdapter类的方法
  • @Configuration
    @EnableWebSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .authorizeRequests()        // 定义哪些URL需要被保护、哪些不需要被保护
                    .anyRequest()               // 任何请求,登录后可以访问
                    .authenticated()
                    .and()
                    .formLogin()
                    .usernameParameter("username")//与登陆页面一致
                    .passwordParameter("password")//与登陆页面一致
                    .loginPage("/login.html")//自定义登陆页面
                    .permitAll()//设置为允许所有人访问
                    .loginProcessingUrl("/securityLogin")//登陆处理请求路径与action一致即可,不需要我们实现
                    .permitAll()//设置为允许所有人访问
                    .and()
                    .logout()//登出配置
                    .logoutUrl("/logout")//不需要我们实现,访问这个自动登出
                    .invalidateHttpSession(true)
                    .deleteCookies("JSESSIONID")
                    .and()
                    .httpBasic()
                    .disable()
                    .csrf()
                    .disable();
        }
    
        /*
         *过滤不需要受保护的资源
         */
        @Override
        public void configure(WebSecurity web) throws Exception {
            web.ignoring()
                    .antMatchers("/js/**")
                    .antMatchers("/css/**")
                    .antMatchers("/image/**");
        }
    
    }
    
    
    运行项目:

    访问接口,自动跳转到自定义登陆页面,输入user、控制台密码

成功跳转到访问的接口

4.定义登陆成功、失败处理器

当我们自定义登陆页面后,会发现当我们登陆失败会重新跳回登陆页面,而没有任何提示,而且我们很多时候都希望前后端通过json传递数据;

  1. 自定义登陆成功处理器,返回json信息,实现AuthenticationSuccessHandler接口并实现
    //登陆账户密码验证成功处理器,返回一个json对象,用于异步处理
    @Component
    public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
        private ObjectMapper objectMapper = new ObjectMapper();
    
        /**
         * @param httpServletRequest
         * @param httpServletResponse
         * @param authentication      认证成功封装的用户信息
         * @throws IOException
         */
        @Override
        public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException {
            //httpServletRequest.getSession().removeAttribute("checkCode");
    //        User user = (User) authentication.getPrincipal();
            //更新最后一次登录时间
            String json = objectMapper.writeValueAsString(authentication.getPrincipal());
    //        System.out.println(json);
            httpServletResponse.setContentType("application/json;charset=utf-8");
            httpServletResponse.getWriter().write(json);
        }
    }
    
    再在配置类配置启用该处理器
    1.获取处理器的bean


    2.配置登陆成功处理器

    修改自己登陆的方式(前端),使用异步方式的表单登陆
    //ajax异步请求进行登陆验证
    function ajaxLogin() {
        $.post("/security/securityLogin", $("#loginForm").serialize(), function (data) {
            console.log(data);
        }, "json");
        return false;//禁止表单提交再次登录
    }

    重新启动登陆:


    可以看到控制台已经打印springSecurity封装的用户信息,登陆成功处理器配置成功

  2. 自定义登陆失败处理器,与成功处理器类似,就不多讲,直接代码

    
    @Component
    public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
        /**
         * @param httpServletRequest
         * @param httpServletResponse
         * @throws IOException
         * @throws ServletException
         */
        @Override
        public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException {
    
            System.out.println(e.getClass()+e.getMessage());
            httpServletResponse.setContentType("application/json;charset=utf-8");
            httpServletResponse.getWriter().write("{\"message\":\""+e.getMessage()+"\"}");
        }
    }



    神奇的是异常信息居然是中文,查了好久才发现是security国际化,之前使用是不会自动转化为中文的
    自定义登陆失败处理器完成

5.添加验证码验证

1.实现提供验证码的api,并在配置类放行允许所有人访问

@Controller
public class CheckCodeController {
    private final Random r = new Random();
    // 随机字符集合中不包括0和o,O,1和l,因为这些不易区分
    private final String codes = "23456789abcdefghijkmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYXZ";

    /**
     * 返回一张图片
     *
     * @param response
     * @param session
     * @throws java.io.UnsupportedEncodingException
     */
    @RequestMapping("/getCheckCode")
    public void getCheckCode(HttpServletResponse response, HttpSession session) throws IOException {
        //request.setCharacterEncoding("UTF-8");
        response.setCharacterEncoding("UTF-8");

        // 设置不缓存图片
        response.setHeader("Pragma", "No-cache");
        response.setHeader("Cache-Control", "No-cache");
        response.setDateHeader("Expires", 0);

        // 指定生成的响应图片,一定不能缺少这句话,否则错误.
        response.setContentType("image/jpeg");
        int width = 80, height = 35; // 指定生成验证码的宽度和高度
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); // 创建BufferedImage对象,其作用相当于一图片
        Graphics g = image.getGraphics(); // 创建Graphics对象,其作用相当于画笔
        Graphics2D g2d = (Graphics2D) g; // 创建Grapchics2D对象

        Random random = new Random();
        Font mfont = new Font("楷体", Font.BOLD, 16); // 定义字体样式
        g.setColor(getRandColor(200, 250));
        g.fillRect(0, 0, width, height); // 绘制背景
        g.setFont(mfont); // 设置字体
        g.setColor(getRandColor(180, 200));

        // 绘制100条颜色和位置全部为随机产生的线条,该线条为2f
        for (int i = 0; i < 100; i++) {
            int x = random.nextInt(width - 1);
            int y = random.nextInt(height - 1);
            int x1 = random.nextInt(6) + 1;
            int y1 = random.nextInt(12) + 1;
            BasicStroke bs = new BasicStroke(2f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL); // 定制线条样式
            Line2D line = new Line2D.Double(x, y, x + x1, y + y1);
            g2d.setStroke(bs);
            g2d.draw(line); // 绘制直线
        }

        //用来保存验证码字符串文本内容
        StringBuilder sb = new StringBuilder();

        for (int i = 0; i < 4; ++i) {// 随机生成4个字符
            String sTemp = String.valueOf(randomChar());
            sb.append(sTemp);

            Color color = new Color(20 + random.nextInt(110), 20 + random.nextInt(110), random.nextInt(110));
            g.setColor(color);
            // 将生成的随机数进行随机缩放并旋转制定角度 PS.建议不要对文字进行缩放与旋转,因为这样图片可能不正常显示

            /* 将文字旋转制定角度 */
            Graphics2D g2d_word = (Graphics2D) g;
            AffineTransform trans = new AffineTransform();
            trans.rotate((45) * 3.14 / 180, 15 * i + 8, 7);

            /* 缩放文字 */
            //float scaleSize = random.nextFloat() + 0.8f;
            //if (scaleSize > 1f)
            float scaleSize = 1.4f;
            trans.scale(scaleSize, scaleSize);
            g2d_word.setTransform(trans);
            g.drawString(sTemp, 15 * i + 5, 14);
        }


        session.setAttribute("checkCode", sb.toString());
        System.out.println("checkCode: " + session.getAttribute("checkCode"));
        //System.out.println("ssRand="+sb.toString());

        g.dispose(); // 释放g所占用的系统资源
        ImageIO.write(image, "JPEG", response.getOutputStream()); // 输出图片
        response.getOutputStream().flush();
    }

    /* 该方法主要作用是获得随机生成的颜色 */
    public Color getRandColor(int s, int e) {
        Random random = new Random();
        if (s > 255) {
            s = 255;
        }
        if (e > 255) {
            e = 255;
        }
        int rr, g, b;
        rr = s + random.nextInt(e - s); // 随机生成RGB颜色中的r值
        g = s + random.nextInt(e - s); // 随机生成RGB颜色中的g值
        b = s + random.nextInt(e - s); // 随机生成RGB颜色中的b值
        return new Color(rr, g, b);
    }

    /*
     *随机获取一个字符
     */
    private char randomChar() {
        int index = r.nextInt(codes.length());
        return codes.charAt(index);
    }

}


2.实现自己的验证码过器链及自定义验证码异常并将验证码过滤器放在登陆的账号密码验证器前面

  • 自定义验证码异常,需继承 AuthenticationException,用来抛出异常给前面自定义登陆失败处理器
     
    //验证码验证错误抛出的异常类
    public class CheckCodeException extends AuthenticationException {
        public CheckCodeException(String msg, Throwable t) {
            super(msg, t);
        }
    
        public CheckCodeException(String msg) {
            super(msg);
        }
    }
  • 实现验证码过滤器,继承OncePerRequestFilter并重写doFilterInternal方法实现自己验证流程
    //验证码验证过滤器,继承OncePerRequestFilter,该过滤器作用于账户密码过滤器前
    @Component
    public class CheckCodeAuthenticationFilter extends OncePerRequestFilter {
    
        @Autowired
        private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            //判断当前请求是否是登陆请求
            if (request.getRequestURI().contains("/securityLogin")) {
                //检验验证码
                try {
                    //用户输入的验证码
                    final String imageCode = request.getParameter("code");
                    //获取系统生成的验证码
                    String key = (String) request.getSession().getAttribute("checkCode");
                    if (key == null || key == "") {
                        throw new CheckCodeException("请勿重复登陆");
                    }
                    if (!imageCode.trim().equalsIgnoreCase(key.trim())) {
                        //抛出自定义验证码异常
                        throw new CheckCodeException("验证码错误");
                    }
                } catch (AuthenticationException e) {
                    //交给authenticationFailureHandler(自定义登陆失败处理器)处理
                    myAuthenticationFailureHandler.onAuthenticationFailure(request, response, e);
                    return;
                }
    
            }
            //验证码正确放行
            filterChain.doFilter(request, response);
        }
    }
    

     

  • 在配置类中将验证码过滤器放在security的过滤链中,并放在靠前面的,账号密码验证之前

    重启项目

    成功!!!

不知不觉很长的博客了,有收获的点个赞,下次得闲写下篇关于security其他配置包括:

  • 自定义登陆数据来源,内存用户数据以及数据库(RBAC权限模型)
  • remenber-me
  • 自定义登出处理器
  • 配置不允许重复登陆
  • 权限不足处理器,未登录处理器
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章