SpringSecurity源码理解及使用(二)

添加验证码

每次登录到达 UsernamePasswordAuthenticationFilter ,都会被拦截 去查询一次数据库就行对比。防止恶意频繁发送错误登录请求,造成数据库大的开销,使用验证码 。 验证码的校验交给 springSecurity去处理,先校验验证码,再查询数据库

验证码的生成

  1. pom依赖
<dependency>
  <groupId>com.github.penggle</groupId>
  <artifactId>kaptcha</artifactId>
  <version>2.3.2</version>
</dependency>
  1. 将kaptcha配置类放入容器
@Configuration
public class KaptchaConfig {
    @Bean
    public Producer kaptcha() {
        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;
    }
}
  1. 编写验证码的controller 将生成的验证码转为流 进行输出
@Controller
public class VerifyCodeController {
    private final Producer producer;
    @Autowired
    public VerifyCodeController(Producer producer) {
        this.producer = producer;
    }

    @RequestMapping("/vc.jpg")
    public void verifyCode(HttpServletResponse response, HttpSession session) throws IOException {

        String verifyCode = producer.createText();
        session.setAttribute("kaptcha",verifyCode); // 这里可以放入redis

        BufferedImage bi = producer.createImage(verifyCode);
		response.setContentType("image/png");
        ServletOutputStream outputStream = response.getOutputStream();

        ImageIO.write(bi,"jpg",outputStream);
        
    }
}

在这里插入图片描述
4. 开放验证码接口 访问权限
在这里插入图片描述

传统web开发

流程:1. 先创建并开放获取图片验证码请求 2、重写 UsernamePasswordAuthenticationFilterattemptAuthentication() 方法(与前后端分离相似)。在查询数据库之前,先校验验证码

  1. 自定义类 MyLoginKaptchaFilter 继承 UsernamePasswordAuthenticationFilter ,重写attemptAuthentication()
    在执行父类的 attemptAuthentication() 方法前 对验证码进行判断 ,如果符合在去调用父类的attemptAuthentication()(查询数据库)
    在这里插入图片描述
    KaptchaFilter
public class KaptchaFilter extends UsernamePasswordAuthenticationFilter {

    public static final String KAPTCHA_KEY = "kaptcha";
    private String kaptcha = KAPTCHA_KEY;

    public String getKaptcha() {
        return kaptcha;
    }

    public void setKaptcha(String kaptcha) {
        this.kaptcha = kaptcha;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {

        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }

        String kaptcha = request.getParameter(getKaptcha());
        String sessionKaptcha = (String) request.getSession().getAttribute("kaptcha");// 若是redis 方式 需要从redis中获取
        if (!ObjectUtils.isEmpty(kaptcha) && !ObjectUtils.isEmpty(sessionKaptcha) &&
                kaptcha.equalsIgnoreCase(sessionKaptcha)) {
            return super.attemptAuthentication(request, response);
        }

        throw new KaptchaNotMatchException("验证码错误");
    }
}

KaptchaNotMatchException 自定义验证异常类

public class KaptchaNotMatchException extends AuthenticationException {


    public KaptchaNotMatchException(String msg, Throwable cause) {
        super(msg, cause);
    }

    public KaptchaNotMatchException(String msg) {
        super(msg);
    }
}
  1. 将自定义的过滤器代替原来的父类
    在这里插入图片描述
 @Bean
    public KaptchaFilter kaptchaFilter() throws Exception {

        KaptchaFilter filter = new KaptchaFilter();
        filter.setAuthenticationManager(authenticationManagerBean());

        filter.setFilterProcessesUrl("/toLogin");
        filter.setPasswordParameter("passwd");
        filter.setUsernameParameter("uname");
        return filter;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests()
                .mvcMatchers("/login.html").permitAll()
                .mvcMatchers("/vc.jpg").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .successForwardUrl("/index.html")
                .loginPage("/login.html")
                .failureUrl("/login.html")
                .successForwardUrl("/index.html")
                .and()
                .csrf().disable();
        http.addFilterAt(kaptchaFilter(), UsernamePasswordAuthenticationFilter.class);
    }

前后端分离开发

与端的不同之处:

  • 传统web开发的参数 直接从请求中获取(getParam(“xxx”)) 前后端分离要从json中获取
  • 后端返回的是json 图片信息也要放在json中(使用Base64)
  • 将处理方式改为响应json格式

在这里插入图片描述

  1. 生成验证码
@RestController
public class VerifyCodeController {
    private final Producer producer;
    @Autowired
    public VerifyCodeController(Producer producer) {
        this.producer = producer;
    }

    @RequestMapping("/vc.jpg")
    public String verifyCode(HttpServletResponse response, HttpSession session) throws IOException {

        String verifyCode = producer.createText();
        session.setAttribute("kaptcha",verifyCode);

        BufferedImage bi = producer.createImage(verifyCode);

        FastByteArrayOutputStream fos = new FastByteArrayOutputStream();

        ImageIO.write(bi,"jpg",fos);

        return Base64.encodeBase64String(fos.toByteArray());
    }
}

注意 前端解析base64编码 要在最前面加上 data:image/png;base64,

  1. 自定义 ** UsernamePasswordAuthenticationFilter** 的 attemptAuthentication() 方法,同时顺手完成前后端分离的认证
    在这里插入图片描述
public class KaptchaFilter extends UsernamePasswordAuthenticationFilter {

    public static final String KAPTCHA_KEY = "kaptcha";
    private String kaptcha = KAPTCHA_KEY;

    public String getKaptcha() {
        return kaptcha;
    }

    public void setKaptcha(String kaptcha) {
        this.kaptcha = kaptcha;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {

        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }

        try {
            Map userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);

            String kaptcha= (String) userInfo.get(getKaptcha());

            String sessionKaptcha = (String) request.getSession().getAttribute("kaptcha");
            if (!ObjectUtils.isEmpty(kaptcha) && !ObjectUtils.isEmpty(sessionKaptcha) &&
                    kaptcha.equalsIgnoreCase(sessionKaptcha)) {
                // 前后端分离 密码 验证
                String username = (String) userInfo.get(getUsernameParameter());
                String passwd = (String) userInfo.get(getPasswordParameter());

                UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, passwd);
                setDetails(request, authRequest);
                return this.getAuthenticationManager().authenticate(authRequest);
            }
            
        } catch (IOException e) {
            e.printStackTrace();
        }

        throw new KaptchaNotMatchException("验证码错误");
    }
}
  1. 将自定义的过滤器代替原来的父类,将相应方式设置为json
  @Bean
    public KaptchaFilter kaptchaFilter() throws Exception {

        KaptchaFilter filter = new KaptchaFilter();
        filter.setAuthenticationManager(authenticationManagerBean());

        filter.setFilterProcessesUrl("/toLogin");
        filter.setPasswordParameter("passwd");
        filter.setUsernameParameter("uname");
        filter.setAuthenticationFailureHandler(((request, response, exception) -> {
            Map<String, Object> result = new HashMap<String, Object>();
            result.put("msg", "登录失败");
            result.put("错误信息", exception.getMessage());
            response.setContentType("application/json;charset=UTF-8");
            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            String s = new ObjectMapper().writeValueAsString(result);
            response.getWriter().println(s);

        }));
        filter.setAuthenticationSuccessHandler((req, resp, authentication) -> {
            Map<String, Object> result = new HashMap<String, Object>();
            result.put("msg", "登录成功");
            result.put("用户信息", authentication.getPrincipal());
            resp.setContentType("application/json;charset=UTF-8");
            resp.setStatus(HttpStatus.OK.value());
            String s = new ObjectMapper().writeValueAsString(result);
            resp.getWriter().println(s);
        });


        
        return filter;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests()
                .mvcMatchers("/login.html").permitAll()
                .mvcMatchers("/vc.jpg").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .successForwardUrl("/index.html")
                .loginPage("/login.html")
                .failureUrl("/login.html")
                .successForwardUrl("/index.html")
                .and()
                .csrf().disable();
        http.addFilterAt(kaptchaFilter(), UsernamePasswordAuthenticationFilter.class);
    }

密码加密

通常使用SHA-256,SHA-512,MD5这些单向算法进行加密,为使其复杂 在加上盐和散列。在用户输入密码后 按照算法进行加盐,散列后与数据库进行对比。为了使其更安全,将盐和密码加密值不放在一个库中。
但仍存在暴力匹对破解情况

springSecurity使用单向自适应函数,特点是

  • 相比于以上算法,即使是单向算法,但每次通过明文生成的加密字符也不相同,无法暴力破解
  • 匹配时故意大量占用系统资源,降低性能,增强安全性

springSecurity默认支持 单向自适应函数类算法是 bcrypt(自身带盐)、PBKDF2。sCrypt, argon2 需要额外引入

密码校验流程

AbstractUserDetailsAuthenticationProvider 提供整个认证流程的整体框架
在这里插入图片描述
调用 passwordEncoder接口的 实现类 。默认是DelegatingPasswordEncoder 负责中转给其他具体算法加密,解密实现类
在这里插入图片描述

DelegatingPasswordEncoder方法中, 根据密码的加密方式(默认在密码前面{加密方式)进行分配对用的算法类
在这里插入图片描述

使用DelegatingPasswordEncoder 这种方式使得密码十分灵活 ,升级十分方便 具体加密方式根据密码前的标识
在这里插入图片描述

如:此次{noop}123 最终匹配校验类
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
springSecurity提供加密的类可以单独拿出来 如:
在这里插入图片描述
在这里插入图片描述
默认散列10次

已定义一种加密方式 +1

默认密码加密方式

默认密码加密方式是采用 DelegatingPasswordEncoder 进行代理,根据密码的首部{}中标识的内容从而再具体选择加密的方式 。
此种方式 使得一个数据库中可以有多种密码加密方式 ,高的兼容性。当设计换掉原来的密码加密方式时,十分的方便

为什么默认是 DelegatingPasswordEncoder

在这里插入图片描述
在这里插入图片描述

指定密码加密方式

根据默认使用DelegatingPasswordEncoder 源码,改变默认条件
在这里插入图片描述
所以 只需要在自定义继承 WebSecurityConfigurerAdapter 的springSecurity的配置类中将 PasswordEncoder 的实现类注入即可
例如 :默认使用 SCryptPasswordEncoder

  @Bean
    public PasswordEncoder passwordEncoder(){
        return new SCryptPasswordEncoder();
    }

只有DelegatingPasswordEncoder 代理类 ,才需要密码前的{xx}去确定真正校验密码的类 ,而直接指定校验密码的类,则密码前不在noop需要{xx}
例如:
直接指定密码不加密 ,则数据源密码无需{noop}

在这里插入图片描述

密码自动迭代更新

在这里插入图片描述

在这里插入图片描述
具体步骤:
1.创建dao层 修改用户名密码的方法
2. 实现UserDetailsPasswordService 接口 ,重写**UserDetails updatePassword(UserDetails user, String newPassword) {}**接口
在这里插入图片描述

@Service
public class MyUserDetailsService implements UserDetailsService , UserDetailsPasswordService {
    private final UserMapper userMapper;
    @Autowired
    public MyUserDetailsService(UserMapper userMapper) {
        this.userMapper = userMapper;
    }
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userMapper.loadUserByUsername(username);
        user.setRoles(userMapper.getRolesByUid(user.getId()));
        return user;
    }

    @Override
    public UserDetails updatePassword(UserDetails user, String newPassword) {

        Integer res = userMapper.updatePassword(user.getUsername(), newPassword);
        if(res==1){
            ((User) user).setPassword(newPassword);
        }
        return user;
    }
}

  1. 同自定义数据源一样,要放入完全的自定义AuthenticationManagerBuilder中 。这一步在设置数据源的时候已经完成
 private final MyUserDetailsService service;
    @Autowired
    public securityConfigure(MyUserDetailsService service) {
        this.service = service;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder builder) throws Exception {
        builder.userDetailsService(service);
    }

在这里插入图片描述

RememberMe

功能类似于 session 存活时的免登陆 (登陆成功后 发送请求的cookie中携带JSESSIONID 能够找到服务器中对应的session,代表已登录),但session放在内存中,耗费系统资源,有过期时间(默认30min)

老式的方法 登陆成功后 将用户名和密码放在cookie中 返回给用户,下次登录携带cookie中的用户名、密码进行数据源校验 (相当于免去自己写账号,密码,基于客户端
springSecurity 方法 基于服务端 ,但服务端无需保存数据。登录成功且选择记住我后,服务端会设置时间,以及用户非敏感信息经过加密(确保不被修改) 通过cookie交给 客户端。客户每次请求,服务端检查cookie信息(但自己不保存),校验合格后,即可认为已登录
类似于JWT
但不安全:可以复制一个网址给浏览器的cookie信息,访该网址问即可获取此用户身份

使用方式

全局
在这里插入图片描述
局部:
在这里插入图片描述

登录页面要加上 <input type="checkbox" name="remember-me"> 也可以自定义name
在这里插入图片描述

记住我流程

注意:调试时设置session 过期时间 ,否则每次session存在都会指定为已登录,不会去使用记住我功能

server:
  servlet:
    session:
      timeout: 1

当开启rememberMe功能时,springSecutiy会加载 RememberMeAuthenticationFilter 过滤器(默认不加载)
在这里插入图片描述
RememberMeAuthenticationFilter 的doFilter()
在这里插入图片描述

请求拦截后,真正负责记住我 功能的类 接口
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

登录认证时 勾选 记住我 流程

在这里插入图片描述

当初次登陆输入账户名、密码 ,勾选记住我功能时 在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

默认方式一:
记住我流程

两种方式在登录成功后 ,生成cookies的分歧点
在这里插入图片描述

默认AbstractRememberMeServices的继承类 TokenBasedRememberMeServices

如果登录时选择记住我会多一次调用数据库
在这里插入图片描述
在这里插入图片描述

cookie值的构成

在这里插入图片描述
cookie在有效期没有重新登录的情况下一城不变

免密登录流程

要在服务器session 过期时测试
免去密码 登录流程

方式 一 和方式二的 分歧点 AbstractRememberMeServices类的processAutoLoginCookie()方法
两个类继承了AbstractRememberMeServices类 对processAutoLoginCookie()的实现不同,从而导致记住我的策略不同

在这里插入图片描述
在这里插入图片描述
等到session 过期 浏览器仍访问其他页面
RememberMeAuthenticationFilter 类中 拦截方法 doFilter()

在这里插入图片描述
最为核心的部分 整体的流程
在这里插入图片描述
简化后:

在这里插入图片描述

  1. 获取对应cookie
    在这里插入图片描述
  2. base64解码拿到基础信息
    在这里插入图片描述
  3. 进行cookie合法校验

在这里插入图片描述
有 key 但 value 不合法时 清除对应cookie
在这里插入图片描述

默认方式的安全问题

当用户认证登录成功且勾选 记住我功能 在浏览器留下
在这里插入图片描述

如果将其复制 ,即可具有此用户身份

在这里插入图片描述
在这里插入图片描述
如果在请求过程中,cookie被劫持,将会引起安全问题
更安全的方式,服务器中保存部分依据,每次请求携带cookie 认证,服务端查找自身的对应依据,认证成功后,返回新的cookie给客户端
使得每次请求都有不同的cookie 如果请求中 的cookie被劫持,非法拿到cookie请求一次后,将会更新cookie。 此用户的cookie无效 ,将被要求重新登录。重新登录又会使得非法拿到的cookie失效

方式二:内存令牌的使用方式

在方式一的基础上 ( .rememberMe())
在这里插入图片描述
方式一和方式二的验证时分歧点在:AbstractRememberMeServices类的processAutoLoginCookie()方法
两个类继承了AbstractRememberMeServices类 对processAutoLoginCookie()的实现不同,从而导致记住我的策略不同

在这里插入图片描述
在进行初次Base64解码后,对cookie的解密方式不同,自然加密方式就不同

生成cookie的分歧点在:
在这里插入图片描述

记住我流程

在这里插入图片描述

具体存储过程在这里插入图片描述

cookie值的构成

和前者MD5层层加密不同,前者一但传给客户端 服务器没有留存,所以全靠加密来保证不被修改
二后者服务器端与留存 ,任何修改导致匹配不成功,cookies作废
服务器端保存 key-value键值对
在这里插入图片描述

免密登录流程

要在服务器session 过期时测试
免去密码 登录流程

和前者在校验cookies方式不同
在这里插入图片描述

存取过程

在这里插入图片描述

在这里插入图片描述
InMemoryTokenRepositoryImpl 类 是由HashMap存储的
在这里插入图片描述
更改存储方式
生成库表的语句 :

create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null);

来自
在这里插入图片描述

方式一:
在这里插入图片描述

方式二:
在这里插入图片描述

最终效果:

在这里插入图片描述

方式二的安全问题

仍会有请求被劫持后,发送一次恶意请求的风险

每一次请求被拦截后 都会先验证session中是否存在用户信息,若没有在 获取cookie ,根据cookie中的用户名查去到完整信息, 无论是否记住我 ,每次都会查询数据库 。最终 实现同登录成功一样的效果。
当手动退出时,会清除记住我的cookies

在这里插入图片描述

传统web开发记住我

如上
在自定义表单时, <input type="checkbox" name="remember-me" value="on|yes|true|1"/> name属性可自定义 ,value值可为 on、yes、true、1

前后端分离开发记住我

在这里插入图片描述

因为在确认 记住我 时要先进行验证,由于前后端分离重写了验证方法,可以在获取前端json参数时,顺便获取 记住我 参数,并将其放入request作用域中,传递下去

整体流程
在这里插入图片描述

具体获取参数并传递
在这里插入图片描述

自定义RememberMeServices的实现的原因

在这里插入图片描述
在这里插入图片描述

如果要重写AbstractRememberMeServices,所有cookies生成、加密、解密等都要自定义,与下面两种方法并列。所以要继承它的子类,只重写rememberMeRequested()

例如 选择默认的cookies生成方式(TokenBasedRememberMeServices
在这里插入图片描述
最后 在security的配置中 明确指出替代原先的类(TokenBasedRememberMeServices)
在这里插入图片描述

在这里插入图片描述
第二篇

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值