基于Security+Redis+jjwt的登录流程

该文章详细介绍了如何配置SpringSecurity以实现无状态的Session管理,并结合JWT进行用户登录认证。配置中包括了自定义的过滤器、异常处理、跨域设置以及权限授权。同时,文章还阐述了登录认证的流程,包括Controller、UserService以及UserDetailsService的角色,使用BCryptPasswordEncoder对密码进行加密,确保安全性。
摘要由CSDN通过智能技术生成


package cn.tedu.tea.admin.server.core.config;

import cn.tedu.tea.admin.server.common.web.JsonResult;
import cn.tedu.tea.admin.server.common.web.ServiceCode;
import cn.tedu.tea.admin.server.core.filter.JwtAuthorizationFilter;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * Spring Security的配置类
 *
 * @author java@tedu.cn
 * @version 1.0
 */
@Slf4j
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启基于方法的安全检查
// @EnableWebSecurity(debug = true) // 开启调试模式,在控制台将显示很多日志,在生产环境中不宜开启
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtAuthorizationFilter jwtAuthorizationFilter;

    public SecurityConfiguration() {
        log.debug("创建配置类对象:SecurityConfiguration");
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        // return NoOpPasswordEncoder.getInstance();
        return new BCryptPasswordEncoder();
    }



    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 配置Security框架不使用Session
        // SessionCreationPolicy.NEVER:从不主动创建Session,但是,Session存在的话,会自动使用
        // SessionCreationPolicy.STATELESS:无状态,无论是否存在Session,都不使用
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        // 将自定义的解析JWT的过滤器添加到Security框架的过滤器链中
        // 必须添加在检查SecurityContext的Authentication之前,具体位置并不严格要求
        http.addFilterBefore(jwtAuthorizationFilter, UsernamePasswordAuthenticationFilter.class);

        // 允许跨域访问,本质上是启用了Security框架自带的CorsFilter
        // 如果不启用CorsFilter,也可以改为对所有OPTIONS请求直接许可,一样可以解决复杂请求预检的跨域问题
        // 注意:即使此处许可以复杂请求的预检,Spring MVC配置类中的启用跨域的配置仍是必须的
        http.cors();

        // 处理“无认证信息却访问需要认证的资源时”的响应
        http.exceptionHandling().authenticationEntryPoint(new AuthenticationEntryPoint() {
            @Override
            public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
                log.warn("{}", e);
                response.setContentType("application/json; charset=utf-8");
                String message = "操作失败,您当前未登录!";
                JsonResult jsonResult = JsonResult.fail(ServiceCode.ERROR_UNAUTHORIZED, message);
                PrintWriter writer = response.getWriter();
                writer.println(JSON.toJSONString(jsonResult));
                writer.close();
            }
        });

        // 白名单
        String[] urls = {
                "/favicon.ico",
                "/doc.html",
                "/**/*.css",
                "/**/*.js",
                "/swagger-resources",
                "/v2/api-docs",
                "/resources/**", // 静态资源文件夹,通常是上传的文件,请与配置文件中的"tea-store.upload.base-dir-name"一致
                "/account/users/login" // 用户登录
        };

        // 禁用“防止伪造的跨域攻击的防御机制”
        http.csrf().disable();

        // 配置请求授权
        // 如果某个请求被多次配置,按照“第一匹配原则”处理
        // 应该将精确的配置写在前面,将较模糊的配置写在后面
        http.authorizeRequests()
                // .mvcMatchers(HttpMethod.OPTIONS, "/**") // 匹配所有OPTIONS类型的请求
                // .permitAll() // 许可
                .mvcMatchers(urls) // 匹配某些请求
                .permitAll() // 许可,即不需要通过认证就可以访问
                .anyRequest() // 任何请求,从执行效果来看,也可以视为:除了以上配置过的以外的其它请求
                .authenticated(); // 需要通过认证才可以访问

        // 是否调用以下方法,将决定是否启用默认的登录页面
        // 当未通过认证时,如果有登录页,则自动跳转到登录,如果没有登录页,则直接响应403
        // http.formLogin();

        // super.configure(http); // 不要调用父类的同名方法,许多默认的效果都是父类方法配置的
    }

}

登录认证流程:

1.controller接收到请求,请求的处理,获取request,通过request.getRemoteAddr();获取用户IP地址,通过request.getHeader(HEADER_USER_AGENT);枚举类:HEADER_USER_AGENT="User-Agent",获取客户端浏览器

@PostMapping("/login")
    @ApiOperation("用户登录")
    @ApiOperationSupport(order = 10)
    public JsonResult login(@Validated UserLoginInfoParam userLoginInfoParam,
                            @ApiIgnore HttpServletRequest request) {
        log.debug("开始处理【用户登录】的请求,参数:{}", userLoginInfoParam);
        String remoteAddr = request.getRemoteAddr();
        String userAgent = request.getHeader(HEADER_USER_AGENT);
        UserLoginResultVO userLoginResultVO = userService.login(userLoginInfoParam, remoteAddr, userAgent);
        return JsonResult.ok(userLoginResultVO);
    }

2.userService处理登录认证

public UserLoginResultVO login(UserLoginInfoParam userLoginInfoParam,
                                   String remoteAddr, String userAgent) {
        log.debug("开始处理【用户登录】的业务,参数:{}", userLoginInfoParam);
        Authentication authentication = new UsernamePasswordAuthenticationToken(
                userLoginInfoParam.getUsername(), userLoginInfoParam.getPassword());
        log.debug("准备调用AuthenticationManager的认证方法,判断此用户名、密码是否可以成功登录……");
        Authentication authenticateResult
                = authenticationManager.authenticate(authentication);
        log.debug("验证用户登录成功,返回的认证结果:{}", authenticateResult);

        Object principal = authenticateResult.getPrincipal();
        log.debug("从认证结果中获取当事人:{}", principal);
        CustomUserDetails userDetails = (CustomUserDetails) principal;
        Long id = userDetails.getId();
        log.debug("从认证结果中的当事人中获取ID:{}", id);
        String username = userDetails.getUsername();
        log.debug("从认证结果中的当事人中获取用户名:{}", username);
        String avatar = userDetails.getAvatar();
        log.debug("从认证结果中的当事人中获取头像:{}", avatar);
        Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();

        log.debug("从认证结果中的当事人中获取权限列表:{}", authorities);
        String authoritiesJsonString = JSON.toJSONString(authorities);
        log.debug("将权限列表对象转换为JSON格式的字符串:{}", authoritiesJsonString);

        Date date = new Date(System.currentTimeMillis() + 1L * 60 * 1000 * durationInMinute);
        //                                                 ↑ 注意加L,避免int溢出为负数
        Map<String, Object> claims = new HashMap<>();
        claims.put("id", id);
        claims.put("username", username);
        // 生成JWT时,不再存入权限列表
        // claims.put("authoritiesJsonString", authoritiesJsonString);
        String jwt = Jwts.builder()
                .setHeaderParam("alg", "HS256")
                .setHeaderParam("typ", "JWT")
                .setClaims(claims)
                .setExpiration(date)
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .compact();

        // 生成JWT之后,需要将权限列表存入到Redis中
        UserLoginInfoPO userLoginInfoPO = new UserLoginInfoPO();
        userLoginInfoPO.setUserAgent(userAgent);
        userLoginInfoPO.setIp(remoteAddr);
        userLoginInfoPO.setAuthoritiesJsonString(authoritiesJsonString);
        userCacheRepository.saveLoginInfo(jwt,userLoginInfoPO);

        // 将用户状态存入到Redis中
        userCacheRepository.saveEnableByUserId(id, 1);

        UserLoginResultVO userLoginResultVO = new UserLoginResultVO()
                .setId(id)
                .setUsername(username)
                .setAvatar(avatar)
                .setToken(jwt);
        return userLoginResultVO;
        // 改为使用JWT后,不必在登录成功后就将认证信息存入到SecurityContext中
        // log.debug("准备将认证信息结果存入到SecurityContext中……");
        // SecurityContext securityContext = SecurityContextHolder.getContext();
        // securityContext.setAuthentication(authenticateResult);
        // log.debug("已经将认证信息存入到SecurityContext中,登录业务处理完成!");
    }

2.1 authenticationManager.authenticate(authentication):Security框架将会自动调UserDetailsService

我这边自定义了一个UserDetailsServiceImpl,将由框架自动调用,下面是具体代码

/**
 * Spring Security处理认证时使用到的获取用户登录详情的实现类
 *
 * @author java@tedu.cn
 * @version 1.0
 */
@Slf4j
@Component
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private IUserRepository userRepository;

    public UserDetailsServiceImpl() {
        log.debug("创建Spring Security的UserDetailsService接口对象:UserDetailsServiceImpl");
    }



    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        log.debug("Spring Security框架自动调用了UserDetailsService对象,将根据用户名获取用户详情,参数:{}", s);
        UserLoginInfoVO loginInfo = userRepository.getLoginInfoByUsername(s);
        log.debug("根据用户名【{}】从数据库中查询用户详情,查询结果:{}", s, loginInfo);

        if (loginInfo == null) {
            return null;
        }

        List<GrantedAuthority> authorities = new ArrayList<>();
        List<String> permissions = loginInfo.getPermissions();
        for (String permission : permissions) {
            GrantedAuthority authority = new SimpleGrantedAuthority(permission);
            authorities.add(authority);
        }

        CustomUserDetails userDetails = new CustomUserDetails(
                loginInfo.getId(), loginInfo.getUsername(), loginInfo.getPassword(),
                loginInfo.getAvatar(), loginInfo.getEnable() == 1, authorities);

        //UserDetails userDetails = User.builder()
        //        .username(loginInfo.getUsername())
        //        .password(loginInfo.getPassword()) // 密文
        //        .disabled(loginInfo.getEnable() == 0) // 账号是否被禁用
        //        .accountLocked(false) // 账号是否被锁定,当前项目中无此概念,则所有账号的此属性都是false
        //        .accountExpired(false) // 账号是否过期,当前项目中无此概念,则所有账号的此属性都是false
        //        .credentialsExpired(false) // 凭证是否过期,当前项目中无此概念,则所有账号的此属性都是false
        //        .authorities(authorities)
        //        .build();
        log.debug("即将向Spring Security框架返回UserDetails类型的结果:{}", userDetails);
        log.debug("接下来,将由Spring Security框架自动验证用户状态、密码等,以判断是否可以成功登录!");
        return userDetails;
    }

}

2.2 该接口实现了UserDetailsService,该接口是Security框架加载用户特定数据的核心接口,重写了接口中loadUserByUsername方法,

2.3 GrantedAuthority表示授予身份认证对象的权限,将从数据库查到的权限放入其中.

2.4 CustomUserDetails 继承自Security的User(User源码的注释:Models core user information retrieved by a UserDetailsService.
Developers may use this class directly, subclass it, or write their own UserDetails implementation from scratch.)

2.5  返回结果给authenticationManager

3 如果认证成果  将继续执行userService的代码,

3.1获取当事人(也就是UserDetailsServiceImpl 返回的userDetails)

3.2从principal中获取相关数据,

3.3配置一个时间,时间中的durationInMinute 为配置文件中提前配置好的以分钟为单位的时间,方便后期维护

3.4使用JJWT工具框架生成jwt(Json Web Token)  JWT中只放id和username即可,因为jwt数据并不是安全的,其中的数据在jwt官网中是可以被解析的,虽然说签名不一致不会导致jwt被盗用,但总归数据还是不能够透露的,签名在配置文件中自定义,以便后期维护.

3.5.考虑到权限列表会占据到很多空间,所以将其存入Redis,以减少数据传输产生的流量,将jwt作为key值,(由dao层userCacheRepository进行处理,注意要在前面加上前缀以分文件夹显示key,否则会导致key不方便在Redis中查找,例如:user:jwt:":"作为默认的分隔符,可以分层显示key)

3.6 封装userLoginInfoPO,作为vlue值

4. 封装登录结果,返回登录结果


注:.密码加密使用BCyrpt算法,与md5算法相比,主要的特点是相同的原文,密文却是不同的,md5采用类似与UUID的结构,相同的原文加密出来的密文是相同的,如果是6位数数字密码普通计算机仅需不到1秒即可暴力求解.而BCrypt字符采用大小写字母数字.和/  密文复杂度高.不易破解.第二个特点是特别慢,默认的构造方法strength值默认为10,2的10次方,意为采用1024次哈希运算,可以在构造方法上更改这个数值让运算速度更慢.从而加大解密的时间,

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
JWT(JSON Web Token)是一种用于身份验证和授权的开放标准。它是一种安全的传输方式,将用户的身份信息进行编码并生成一个令牌,可以在客户端和服务器之间进行传递。JWT通常由三部分组成:头部,载荷和签名。头部包含了令牌的类型和加密算法,载荷包含了用户的身份信息,签名用于验证令牌的合法性和完整性。 Spring Security是一个基于Spring框架的安全性解决方案,提供了一套全面的认证和授权机制。它集成了JWT作为一种认证方式,可以通过JWT来进行用户身份验证和授权。Spring Security可以提供用户认证、授权、密码加密、会话管理等功能。 Redis是一种内存数据库,它支持高性能的键值对存储,并提供了多种数据结构的支持。在商城系统中,Redis通常被用作缓存,用于存储用户的登录信息、购物车数据、商品库存等。通过将数据存储在内存中,Redis能够提供非常快速的读写性能,从而提升系统的响应速度和并发能力。 综上所述,JWT是一种用于身份验证和授权的开放标准,可以与Spring Security集成来实现安全认证和授权机制。而Redis则可以作为缓存数据库,用于提升系统性能和数据访问速度。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [基于SpringBoot2+MybatisPlus+SpringSecurity+jwt+redis+Vue的前后端商城系统源码](https://download.csdn.net/download/2301_76965813/87778818)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [SpringBoot2+MybatisPlus+SpringSecurity+jwt+redis+Vue的前后端分离的商城系统](https://download.csdn.net/download/weixin_47367099/85250567)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [通用权限管理系统+springboot+mybatis plus+spring security+jwt+redis+mysql](https://download.csdn.net/download/qq_37049128/87842802)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值