Springboot整合SpringSecurity二

SpringBoot整合SpringSecurity二

在上一章入门案例 中,我们实现了入门程序,本篇我们在上一章的基础上完成自动登录功能及异常处理。

本案例源码地址:https://gitee.com/lin8081/LWH11

1.自动登录

所谓自动登陆就是当用户第一次访问网站时,输入用户名和密码,然后勾选了自动登陆复选框,进入首页后,点击退出登陆,关闭网页,再次打开同样的网站,则无需再次输入账号密码,直接进入首页,这种交互方式就是“自动登录”。

1.修改登录页面:login.html

<form method="post" action="/login">

<!-- 账号 -->
<div class="layui-form-item">
     <label class="layui-form-label">账号</label>
     <div class="layui-input-block">
        <input name="username" id="userName"  value="admin" placeholder="默认账号:admin" type="text" lay-verify="required" class="layui-input">
     </div>
</div>

<!-- 密码 -->
<div class="layui-form-item">
     <label class="layui-form-label">密码</label>
      <div class="layui-input-block">
          <input name="password" id="password"  value="123" placeholder="默认密码:123" type="password" lay-verify="required" class="layui-input">
      </div>
</div>
  
<!-- 自动登录 -->  
<div class="layui-form-item">
      <label><input style="display: inline" type="checkbox" name="remember-me"/>自动登录</label>
</div>
                    
<div>
      <button type="submit" class="layui-btn layui-btn-fluid" >登 录</button>
</div>

</form>

2.自动登录的实现方式—数据库存储

使用 Cookie 存储虽然方便,但是 Cookie 毕竟是保存在客户端的,而且 Cookie 的值还与用户名、密码这些敏感信息有关,虽然加密了,但是将这些敏感信息存在客户端,毕竟不太保险。

**SpringSecurity 还提供了另一种相对安全的实现机制: **

  • 在客户端的 Cookie中,仅保存一个无意义的加密串(与用户名和密码等敏感信息无关),然后在数据库中保存该加密串 - 用户信息的对应关系,自动登录时,用 Cookie 中的加密串,到数据库验证,如果通过,自动登录才算成功。
1.基本原理

当浏览器发起表单登录请求时,当通过 UsernamePasswordAuthenticationFilter 认证成功后,会经过 RememberMeService, 在其中有个 TokenRepository , 它会生成一个 token, 首先将 token 写入到浏览器的 Cookie中,然后将 token、认证成功的用户名写入到数据库中。

当浏览器下次请求时,会经过 RememberMeAuthenticationFilter,它会读取 Cookie 中的 token,交给 RememberMeService ,获取用户信息,并将用户信息放入到 SpringSecurity 中,实现自动登录。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lk1OUSjB-1584085522201)(assets/1583765716646.png)]

RememberMeAuthenticationFilter在整个过滤器链中是比较靠后的位置,也就是说在传统的登录方式都无法登录情况下才会使用自动登录。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S4TsyYmA-1584085522204)(assets/1583765815108.png)]

3.创建token数据库表

可由以下代码进行创建token表,但数据库表时已经存在,请注释掉,否则会报错

tokenRepository.setCreateTableOnStartup(true);

也可以直接sql语句创建表结构

CREATE TABLE `persistent_logins` (
  `username` varchar(64) NOT NULL,
  `series` varchar(64) NOT NULL,
  `token` varchar(64) NOT NULL,
  `last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

4.代码实现

在 WebSecurityConfig 中注入 dataSource ,创建一个 PersistentTokenRepository 的Bean对象:

@Autowired
    private DataSource dataSource;

    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);
        // 如果token表不存在,使用下面可以自动初始化表结构,如果已经存在,请注释掉,否则会报错
        // tokenRepository.setCreateTableOnStartup(true);
        return tokenRepository;
    }

在 config() 中配置自动登录:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                // 如果有允许匿名的url,填在下面
//                .antMatchers().permitAll()
                .anyRequest().authenticated()
                .and()
                // 设置登陆页
                .formLogin().loginPage("/login")
                // 设置登陆成功页
                .defaultSuccessUrl("/").permitAll()
                // 自定义登陆用户名和密码参数,默认为username和password
//                .usernameParameter("username")
//                .passwordParameter("password")
                .and()
                .logout().permitAll()

                // 自动登录
                .and().rememberMe()
                .tokenRepository(persistentTokenRepository())
                // token有效时间,单位:s
                .tokenValiditySeconds(60)
                .userDetailsService(userDetailsService);

        // 关闭CSRF跨域
        http.csrf().disable();
    }

3.运行测试

勾选自动登录后,Cookie 和数据库中均存储了 token 信息:

在这里插入图片描述

2.异常处理

我们登录失败的时候,SpringSecurity 帮我们跳转到了 /login?error URL,奇怪的是不管是控制台还是网页上都没有打印错误信息。

这是因为首先 /login?error 是SpringSecurity 默认的失败 URL,其次如果你不自己处理这个异常,这个异常时不会被处理的。

1.常见异常

我们先来列举下一些 SpringSecurity 中常见的异常:

  • UsernameNotFoundException (用户不存在)
  • DisableException(用户已被禁用)
  • BadCredentialsException(坏的凭据)
  • LockedException(账号锁定)
  • CerdentialsExpiredException(证书过期)
  • … 以上列出的这些异常都是 AuthenticationException 的子类,然后我们看 SpringSecurity 是如何处理 AuthenticationException 异常的。

2.源码分析

SpringSecurity的异常处理是在过滤器中进行的,我们在 AbastrctAuthenticationProcessingFilter 中找到了对 Authentication 的处理:

  • 在 doFilter() 中,捕获 AuthenticationException 异常,并交给 unsuccessfulAuthentication() 处理。

  • unsuccessfulAuthentication() 中,转交给了 SimpleUrlAuthenticationFailureHandler 类的 onAuthencicationFailure() 处理。

  • 在 onAuthenticationFailure() 中,首先判断有没有设置 defaultFailureUrl

    a. 如果没有设置,直接返回 401 错误,即 HttpStatus.UNAUTHORIZED 的值。 b. 如果设置了,首先执行 saveException() 方法。然后判断 forwardToDestination 是否为服务器调整,默认使用重定向即客户端跳转。

  • 在 saveException() 方法中,首先判断 forwardToDestination,如果使用服务器跳转则写入Request,客户端跳转则写入 Session。写入名为 WebAttributes.AUTHENTICATION_EXCEPTION 常量对应值SPRING_SECURITY_LAST_EXCEPTION,值为 AuthenticationException 对象。

  • 至此 SpringSecurity 完成了异常处理,总结下流程:

    –> AbstractAuthenticationProcessingFilter.doFilter() –> AbstractAuthenticationProcessingFilter.unsuccessfulAuthentication() –> SimpleUrlAuthenticationFailureHandler.onAuthenticationFailure() –> SimpleUrlAuthenticationFailureHandler.saveException()

3.处理异常

上面通过源码看着挺复杂,但真正处理起来SpringSecurity为我们提供了方便的方式,我们只需要指定错误的url,然后在该方法中对异常进行处理即可。

  • 指定错误url ,在WebSecurityConfig 中添加 .failureUrl("/login/error")
@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                // 如果有允许匿名的url,填在下面
//                .antMatchers().permitAll()
                .anyRequest().authenticated()
                .and()
                // 设置登陆页
                .formLogin().loginPage("/login")
                // 设置登陆成功url
                .defaultSuccessUrl("/").permitAll()
                // 设置登录失败url
                .failureUrl("/login/error")
                // 自定义登陆用户名和密码参数,默认为username和password
//                .usernameParameter("username")
//                .passwordParameter("password")
                .and()
                .logout().permitAll()
                // 自动登录
                .and().rememberMe()
                .tokenRepository(persistentTokenRepository())
                // 有效时间,单位:s
                .tokenValiditySeconds(60)
                .userDetailsService(userDetailsService);

        // 关闭CSRF跨域
        http.csrf().disable();
    }

  • 在 Controller 中编写 loginError方法完成异常处理操作:

    @GetMapping("/login/error")
        @ResponseBody
        public Result loginError(HttpServletRequest request) {
            AuthenticationException authenticationException = (AuthenticationException) request.getSession().getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
            log.info("authenticationException={}", authenticationException);
            Result result = new Result();
            result.setCode(201);
    
            if (authenticationException instanceof UsernameNotFoundException || authenticationException instanceof BadCredentialsException) {
                result.setMsg("用户名或密码错误");
            } else if (authenticationException instanceof DisabledException) {
                result.setMsg("用户已被禁用");
            } else if (authenticationException instanceof LockedException) {
                result.setMsg("账户被锁定");
            } else if (authenticationException instanceof AccountExpiredException) {
                result.setMsg("账户过期");
            } else if (authenticationException instanceof CredentialsExpiredException) {
                result.setMsg("证书过期");
            } else {
                result.setMsg("登录失败");
            }
            return result;
        }
    

    修改 CustomUserDetailsService loadUserByUsername() 方法的返回值:

     @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            Collection<GrantedAuthority> authorities = new ArrayList<>();
            // 从数据库中取出用户信息
            SysUser user = userService.selectByName(username);
    
            // 判断用户是否存在
            if(user == null) {
                throw new UsernameNotFoundException("用户名不存在");
            }
    
            // 添加权限
            List<SysUserRole> userRoles = userRoleService.listByUserId(user.getId());
            for (SysUserRole userRole : userRoles) {
                SysRole role = roleService.selectById(userRole.getRoleId());
                authorities.add(new SimpleGrantedAuthority(role.getName()));
            }
    
            // 返回UserDetails实现类
            return new User(user.getUsername(), user.getPassword(),true,
                    true,
                    true,
                    true,
                    authorities);
        }
    

4.运行项目

账号密码错误情况下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4vfxRcsV-1584085522208)(assets/1583778922802.png)]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值