登录认证过程的解读

登录是每个web应用必不可少的第一步,接下来我来分析登录过程的操作步骤以及需要配置的东西,话不多说,直接开搞!

首先,我来说一下登录的大概流程:

        1.检验验证码

        2.用户认证

其实细想就分这两大步,但是要写起来,对于我这个小白还是挺困难的,主要是登录逻辑的一些细节处理是欠缺的,下面我们来边看代码边理解内在一些逻辑。

这是登录的主体代码:

public String login(String username, String password, String code, String uuid) {
        boolean enabled = configService.selectCaptchaEnabled();
        //校验 验证码
        if(enabled){
            validateCaptcha(username,code,uuid);
        }
        Authentication authentication = null;
        try {
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username,password);
            AuthenticationContextHolder.setContext(authenticationToken);
            authentication = authenticationManager.authenticate(authenticationToken);

        } catch (AuthenticationException e) {
            if(e instanceof BadCredentialsException){
                systemLogService.insertLog(username,"用户匹配","usernamePasswordNotMatch");
            }
            else {
                systemLogService.insertLog(username,"认证失败","Authentication error");
            }

        }
        finally {
            AuthenticationContextHolder.clearContext();
        }
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        recordLoginInfo(loginUser.getUserId());
        return tokenService.createToken(loginUser);
    }

我们来一句一句分析一下作用:

boolean enabled = configService.selectCaptchaEnabled();

这段代码作用是:查看验证码是否开启(这个在 生成验证码的blog中是有描述的),这里在简单说一下,其实就是去redis cache中查一个value,然后根据value值判断是否开启(例如:yes - 开启;no - 关闭;null - 开启)。

validateCaptcha(username,code,uuid);

 public void validateCaptcha(String username,String code,String uuid){
        String CaptchaKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.paramNotNull(uuid,"");
        String value = redisCache.getObjectByKey(CaptchaKey);
        log.info("code值为{}",value);
        //手动添加
        redisCache.deleteObject(CaptchaKey);
        if(CaptchaKey == null){
            //记录信息
            systemLogService.insertLog(username,"验证码查询","未找到验证码或验证码没有");
            //要想不抛出异常  需要extends RunnableException
            throw new UserException("key is null");
        }
        if(!value.equalsIgnoreCase(code)){
            systemLogService.insertLog(username,"校验验证码","验证码输入错误");
            throw  new UserException("code is error");
        }

这段代码作用:校验验证码。这里可以对照生成验证码看,生成验证码他的形式就是:PREIX(一个自定义前缀) + uuid,这里其实就是怎么存进去的,怎么取出来,取到value值之后,记得删除(可能这个小细节很重要,因为如果你不删除的话,那么缓存还存在,再进行登录的时候,会导致,输入上一个value也能进入,这是一个很严重的安全问题),下面就是两个判断,其实若依框架使用异步多线程来记录信息,这里我用的是自己建的一个log 数据库,然后我直接insert,这是比较简单的实现方法(因为不是很理解异步多线程)。

用户认证:

 UsernamePasswordAuthenticationToken authenticationToken = new                                        UsernamePasswordAuthenticationToken(username,password);
AuthenticationContextHolder.setContext(authenticationToken);
authentication = authenticationManager.authenticate(authenticationToken);

第一句:其实就是一个未认证的authentication,啥也没干,可能就是包装一下,可以看一下源码,其实就是往构造器赋值的过程,不多说了。

第二句:往上下文中加入未认证的authenticationToken。

第三句:经过认证的authentication,authenticationManager对象会调用你重写的那个myUserDetailsService里面的loadUserByUsername(),往里面封装了一个userDetails(其实就是登录用户的信息,例如userId、username、password、status、emails等等)。

recordLoginInfo(loginUser.getUserId());

 private void recordLoginInfo(Long UserId) {
        User user = new User();
        user.setUserId(UserId);
        user.setLoginDate(new Date());
        user.setUpdateTime(new Date());
        userService.updateUserProfile(user);

    }

这段代码是更新一下用户的信息,其实就是时间、id、ip等等。

return tokenService.createToken(loginUser);
public String createToken(LoginUser loginUser){
        String token = StringUtils.simpleUUID();
        loginUser.setToken(token);
        refreshToken(loginUser);
        Map<String ,Object> claims = new HashMap<>();
        claims.put(Constants.LOGIN_USER_KEY,token);
        return createToken(claims);
    }

最后创建token,简单来说就是一个uuid,然然后利用jwt生成token。

 public String createToken(Map<String ,Object> claims){
        String token = Jwts.builder()
                .setClaims(claims)
                .signWith(SignatureAlgorithm.HS256,secret).compact();
        return token;

我们需要在配置文件中指定一个秘钥

token:
  #过期时间
  expireTime: 30
  #令牌秘钥
  secret: abcdefghijklmnopqrstuvwxyz
  #令牌自定义标识
  header: Authorization

以上是token 需要在配置文件中配置的参数

/**、
     * 刷新token过期时间
     * @param loginUser
     */
    private void refreshToken(LoginUser loginUser) {
        loginUser.setLoginTime(System.currentTimeMillis());
        loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
        String key = getTokenKey(loginUser.getToken());
        redisCache.setObjectByKey(key,loginUser,expireTime, TimeUnit.MINUTES);
    }

可以看到是set一个登录时间和过期时间,然后将key(形式是:LOGIN_TOKEN_KEY(前缀)+ token)value是loginUser ,过期时间是常量expireTime(我设置的30分钟)。

主体代码就是这些,下面我们来说一下配置:

首先说一下用户认证我用的是SpringSecurity,本质上就是一系列Filter(过滤器),很通俗的理解就是设置过滤器(addFilter),然后过滤器执行类中的处理方法,然后放行。当然,最重要的肯定是处理方法了。

@Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .headers().cacheControl().disable().and()
                .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).and()
                // 基于token,所以不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                // 过滤请求
                .authorizeRequests()
                // 对于登录login 注册register 验证码captchaImage 允许匿名访问
                .antMatchers("/login", "/register", "/captchaImage","/test").permitAll()
                // 静态资源,可匿名访问
                .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
                .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated()
                .and()
                .headers().frameOptions().disable();//防止iframe内容无法显示,解决问题!
                // 添加Logout filter
        http.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        // 添加CORS filter 跨域指的是协议,域名,端口其中一个不同  就不允许通讯(同源通讯)
        http.addFilterBefore(corsFilter,JwtAuthenticationTokenFilter.class);
        http.addFilterBefore(corsFilter, LogoutFilter.class);


                //这两个参数其实不用写,在UsernamePasswordAuthenticationFilter过滤器中会默认表单提交为username ; password
//                .passwordParameter()
//                .usernameParameter()
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }
    @Bean
    public BCryptPasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

其中有两个Bean的注入:

1.AuthenticationManager:作用:认证信息。它最后会调用UserDetailsService中的loadUserByUsername方法。

2.BCryptPasswordEncoder :密码加密的工具

还有两个configure:

@Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

 通过debug可以看到他是指定了处理问题的方法,auth是一个适配器,里面有passwordEncoder

和userService,我们可以这么理解,我们把我们要指定的加密工具和重写的userService处理用户自定义认证全都给了auth,认证过程都要调用我们指定的方法。

// 添加Logout filter
        http.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        // 添加CORS filter 跨域指的是协议,域名,端口其中一个不同  就不允许通讯(同源通讯)
        http.addFilterBefore(corsFilter,JwtAuthenticationTokenFilter.class);
        http.addFilterBefore(corsFilter, LogoutFilter.class);

这是添加过滤器的步骤:

登出处理器:

LoginUser loginUser = tokenService.getLoginUser(request);
        if(StringUtils.isNotNull(loginUser)){
            tokenService.delLoginUser(loginUser.getToken());
            logService.insertLog(loginUser.getUsername(), "用户退出","成功退出");
        }
        response.getWriter().print(AjaxResult.success("退出成功"));

很简单的逻辑,通过request获取到token,然后删掉用户token缓存。

token认证过滤器:

LoginUser loginUser = tokenService.getLoginUser(request);
        if(StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication())){
            tokenService.verifyToken(loginUser);
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
        filterChain.doFilter(request,response);

这个过滤器是浏览器发送请求到服务器(带着token),需要验证token的有效性,我们通过debug模式直观的通过数据来分析:

 我们可以看到更新了token的过期时间,具体 代码见下:

  /**
     *验证令牌有效期,不足20分钟,自动刷新
     * @param user
     */
    public void verifyToken(LoginUser user){
        Long expireTime = user.getExpireTime();
        long currentTimeMillis = System.currentTimeMillis();
        if(expireTime - currentTimeMillis <= MILLIS_MINUTE_TEN){
            refreshToken(user);
        }

我们可以看到,loginUser信息被设置进去,并且 authenticated的状态呗设置为true,也就是已认证。

detail设置了remoteAddress。

SecurityContextHolder.getContext().setAuthentication(authenticationToken);

 最后将authenticationToken添加到security上下文中。

最后一个CorsFilter 是为了解决跨域问题,最重要的是他需要加载上面两个过滤器前面,要么无法起作用。(可以百度一下什么是跨域,这里就不解释了)。

总结:

其实对于我这个小白来说,很重要的是对于一些数据的处理、判断。比如,登录出问题了,之前我们解决问题的办法可能就是简单的print,但是现在我们在web开发,需要记录日志,来便利我们的一些要求,所以首先要掌握大题逻辑,然后对于细节的处理还是要多学。

声明:以上好多是个人见解,如有错误,大佬请多多指教~

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值