Spring security+JWT+Redis实现验证,授权

Spring security登录流程:

在这里插入图片描述

Token:

Token机制是用于解决服务器端识别客户端身份相关问题的。
当某个客户端向服务器端发起登录的请求时,将直接发起请求,当服务器端收到此请求时,如果判断登录成功,会在响应时返回一个Token值,当客户端收到Token后,后续的访问都会自动携带此Token到服务器端,则服务器端可以根据这个Token值来识别客户端的身份。
与Session不同,Token是由服务器端的程序(开发者自行编写的)生成的数据,此数据是一段有意义的数据,例如你可以把用户的ID、用户名都存放到Token中,则在后续的访问中,客户端携带了Token后,服务器端可以直接从Token中找到相关信息,例如用户的ID、用户名等等,从而,服务器端的内存中,并不需要持续的保存相关信息,所以,Token可以被设置一段非常长的有效期,并且不用担心持续性的消耗服务器端内存的问题。
基于Token的特征,它可以解决Session能解决的问题,并且,天生就适用于集群或分布式系统,只要集群或分布式系统中的各个服务器具有相同的检查Token和解析Token的程序即可

使用jwt:

当验证客户端的登录信息成功后,将生成JWT数据,并响应到客户端去,类似于“买票”的过程
客户端自主携带JWT来向服务器端发起请求,服务器端则需要尝试接收并尝试解析JWT,类似于“验票”的过程
解析成功后,会得到JWT中的数据信息,需要将这些信息创建为Authentication对象并存入到SecurityContext中。

1.准备工作

1.1yml配置文件

server:
  port: 8088

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/ylmall?allowPublicKeyRetrieval=true&useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&allowMultiQueries=true
    username: root
    password: 123456
    druid:
      initial-size: 5
      max-active: 20
  redis:
    host: localhost
    port: 6379
    username: ~
    password: ~

# MyBatis Plus
mybatis-plus:
  mapper-locations: classpath:mappers/*.xml
# Knife4j配置
knife4j:
  # 开启增强模式
  enable: true
logging:
  # 显示级别
  level:
    # 某根包(配置将应用于此包及子孙包)的显示级别
    cn.ylz.ylmall: trace
ylmall:
  jwt:
    # JWT的有效时长,以分钟为单位
    duration-in-minute: 86400
    # JWT的secretKey
    secret-key: fNesMDkqrJFdsfDSwAbFLJ8SnsHJ438AF72D73aKJSmfdsafdLKKAFKDSJ

在使用knife4j时要配置增强模式,enable: true

1.2SQL代码

基于RBAC(Role-Based Access Control:基于角色的访问控制)的权限设计

<!--用户表-->
DROP TABLE IF EXISTS account_user;
CREATE TABLE account_user
(
    id           BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '数据ID',
    username     VARCHAR(50)         DEFAULT NULL COMMENT '用户名',
    password     CHAR(64)            DEFAULT NULL COMMENT '密码',
    nickname     VARCHAR(50)         DEFAULT NULL COMMENT '昵称',
    avatar       VARCHAR(255)        DEFAULT NULL COMMENT '头像URL',
    phone        VARCHAR(50)         DEFAULT NULL COMMENT '手机号码',
    email        VARCHAR(50)         DEFAULT NULL COMMENT '电子邮箱',
    description  VARCHAR(255)        DEFAULT NULL COMMENT '个人简介',
    enable       TINYINT(3) UNSIGNED DEFAULT '0' COMMENT '是否启用,1=启用,0=未启用',
    gmt_create   DATETIME            DEFAULT NULL COMMENT '数据创建时间',
    gmt_modified DATETIME            DEFAULT NULL COMMENT '数据最后修改时间',
    PRIMARY KEY (id)
) DEFAULT CHARSET = utf8mb4 COMMENT ='用户';

<!--角色表-->
DROP TABLE IF EXISTS account_role;
CREATE TABLE account_role
(
    id           BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '数据ID',
    name         VARCHAR(50)         DEFAULT NULL COMMENT '名称',
    description  VARCHAR(255)        DEFAULT NULL COMMENT '简介',
    sort         TINYINT(3) UNSIGNED DEFAULT '0' COMMENT '排序序号',
    gmt_create   DATETIME            DEFAULT NULL COMMENT '数据创建时间',
    gmt_modified DATETIME            DEFAULT NULL COMMENT '数据最后修改时间',
    PRIMARY KEY (id)
) DEFAULT CHARSET = utf8mb4 COMMENT ='角色';

<!--用户角色表-->
DROP TABLE IF EXISTS account_user_role;
CREATE TABLE account_user_role
(
    id           BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '数据ID',
    user_id      BIGINT(20) UNSIGNED DEFAULT NULL COMMENT '用户ID',
    role_id      BIGINT(20) UNSIGNED DEFAULT NULL COMMENT '角色ID',
    gmt_create   DATETIME            DEFAULT NULL COMMENT '数据创建时间',
    gmt_modified DATETIME            DEFAULT NULL COMMENT '数据最后修改时间',
    PRIMARY KEY (id)
) DEFAULT CHARSET = utf8mb4 COMMENT ='用户角色关联';

<!--权限表-->
DROP TABLE IF EXISTS account_permission;
CREATE TABLE account_permission
(
    id           BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '数据ID',
    name         VARCHAR(50)         DEFAULT NULL COMMENT '名称',
    value        VARCHAR(255)        DEFAULT NULL COMMENT '值',
    description  VARCHAR(255)        DEFAULT NULL COMMENT '简介',
    sort         TINYINT(3) UNSIGNED DEFAULT '0' COMMENT '排序序号',
    gmt_create   DATETIME            DEFAULT NULL COMMENT '数据创建时间',
    gmt_modified DATETIME            DEFAULT NULL COMMENT '数据最后修改时间',
    PRIMARY KEY (id)
) DEFAULT CHARSET = utf8mb4 COMMENT ='权限';

<!--角色权限表-->
DROP TABLE IF EXISTS account_role_permission;
CREATE TABLE account_role_permission
(
    id            BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '数据id',
    role_id       BIGINT(20) UNSIGNED DEFAULT NULL COMMENT '角色ID',
    permission_id BIGINT(20) UNSIGNED DEFAULT NULL COMMENT '权限ID',
    gmt_create    DATETIME            DEFAULT NULL COMMENT '数据创建时间',
    gmt_modified  DATETIME            DEFAULT NULL COMMENT '数据最后修改时间',
    PRIMARY KEY (id)
) DEFAULT CHARSET = utf8mb4 COMMENT ='角色权限关联';

INSERT INTO account_user
VALUES (1, 'root', '$2a$10$N.ZOn9G6/YLFixAOPMg/h.z7pCu6v2XyFDtC4q.jeeGm/TEZyj15C', '系统用户',
        'https://img1.baidu.com/it/u=172033757,2398290767&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', '13900139001',
        'root@baidu.com', '最高权限的用户', 1,'2023-06-01 08:00:00', '2023-06-01 08:00:00');

INSERT INTO account_role
VALUES (1, '系统管理员', '最高权限的管理员角色,应该关联所有权限', 255, '2023-06-01 08:00:00', '2023-06-01 08:00:00')

INSERT INTO account_user_role
VALUES (1, 1, 1, '2023-06-01 08:00:00', '2023-06-01 08:00:00');

INSERT INTO account_permission
VALUES (1, '账号管理-用户-读取', '/account/user/read', '读取用户信息,含查看列表、查看详情,及其它查询操作', 255, '2023-06-01 08:00:00',
        '2023-06-01 08:00:00'),
       (2, '账号管理-用户-添加', '/account/user/add-new', '添加用户', 254, '2023-06-01 08:00:00', '2023-06-01 08:00:00'),
       (3, '账号管理-用户-删除', '/account/user/delete', '删除用户', 253, '2023-06-01 08:00:00', '2023-06-01 08:00:00'),
       (4, '账号管理-用户-修改', '/account/user/update', '修改用户信息,含修改密码、启用、禁用、修改基本资料,及其它修改操作', 252, '2023-06-01 08:00:00',
        '2023-06-01 08:00:00');

INSERT INTO account_role_permission
VALUES (1, 1, 1, '2023-06-01 08:00:00', '2023-06-01 08:00:00'),
       (2, 1, 2, '2023-06-01 08:00:00', '2023-06-01 08:00:00'),
       (3, 1, 3, '2023-06-01 08:00:00', '2023-06-01 08:00:00'),
       (4, 1, 4, '2023-06-01 08:00:00', '2023-06-01 08:00:00');

1.3pom.xml文件相关依赖

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

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

        <!-- Spring Boot中的Spring MVC的依赖项,用于处理请求、响应结果、统一处理异常 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- Knife4j Spring Boot:在线API文档 -->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-spring-boot-starter</artifactId>
            <version>2.0.9</version>
        </dependency>
        <!-- Lombok:便捷的编写POJO类 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.20</version>
        </dependency>
        <!-- MySQL -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
            <dependency>
                <groupId>javax.xml.bind</groupId>
                <artifactId>jaxb-api</artifactId>
                <version>2.3.1</version>
            </dependency>

        <!-- Druid:数据库连接池框架 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.16</version>
        </dependency>
        <!-- MyBatis Plus整合Spring Boot的依赖项 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.3.0</version>
        </dependency>
        <!-- Spring Boot中的Spring Validation的依赖项,用于检查方法的参数的基本有效性 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <!-- Spring Boot中的Spring Security的依赖项,用于处理认证与授权 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- fastjson:实现对象与JSON的相互转换 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.75</version>
        </dependency>
        <!-- PageHelper:专用于MyBatis的分页框架 -->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.3.0</version>
        </dependency>
        <!-- JJWT(Java JWT) -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <!-- Spring Boot支持Redis编程的依赖项 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
    </dependencies>

2.代码编写

2.1 SecurityConfiguration配置类

此时运行项目,控制台就会生成一个security的登录密码,访问所有的请求时都需要进行登录才能访问。因此我们需要先写一个配置类继承 WebSecurityConfigurerAdapter,重写configure(HttpSecurity http)方法。

  1. 在配置类里配置好白名单(不需要登录就能访问的,此时Get请求可以,Post请求还是不行)
  2. 禁用防止伪造的跨域攻击的防御机制(此时登录成功后,post请求就可以正常访问)
  3. 启用跨域访问,不使用session(已经使用JWT就不再使用session来占用内存空间)
  4. 密码加密passwordEncoder()以及认证管理器authenticationManagerBean()
@Slf4j
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Autowired
    private JwtAuthorizationFilter jwtAuthorizationFilter;
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 配置Security框架不使用Session
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        // 将自定义的解析JWT的过滤器添加到Security框架的过滤器链中
        http.addFilterBefore(jwtAuthorizationFilter, UsernamePasswordAuthenticationFilter.class);
        // 允许跨域访问,本质上是启用了Security框架自带的CorsFilter
        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/**",
                "/v1/users/login",
                "/v1/users/admin_reg",
                "/v1/contents/post-content",
                "/v1/contents/{id}/delete",
                "/v1/users/verifyCode",
                "/v1/users/reg",
        };
        http.authorizeRequests()
                .mvcMatchers(urls)
                .permitAll()
                .anyRequest()
                .authenticated();
        // 禁用防止伪造的跨域攻击的防御机制(此时登录成功后,post请求就可以正常访问)
        http.csrf().disable();
    }
}

2.2 实现UserDetailsService接口

Spring Security提供了一个UserDetailsService接口,我们需要实现这个接口的loadUserByUsername方法,用于用户信息的获取,实现根据用户名从数据库中来查询用户详情。如果用户不存在返回null,后续认证中有全局异常处理机制处理异常。

(用户状态1为启用,0为禁用)

@Slf4j
@Component
public class UserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private IUserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        UserLoginInfoVO infoVO = userRepository.selectByUsername(s);
        log.debug("infovo,{}",infoVO);

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

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

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

2.3 自定义的userDetails

返回数据库中查询到的userDetails

自定义的用户详细信息类,包含了用户的额外信息,如ID、头像等.扩展UserDetails类型,使得可以向JWT中存入用户ID;UserDetails就是认证结果中的当事人.需要继承User或实现UserDetails

@Getter
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class CustomUserDetails extends User {
    private Long id;
    private String avatar;

    public CustomUserDetails(Long id,
                             String username,
                             String password,
                             String avatar,
                             boolean enabled,
                             Collection<? extends GrantedAuthority> authorities) {
        super(username, password, enabled, true, true, true, authorities);
        this.id = id;
        this.avatar = avatar;
    }
}

2.4 dao层代码

dao层只需要实现根据用户名从数据库中查询用户详情信息就OK

<!--UserLoginVO selectByUsername(String s);-->
    <select id="selectByUsername" resultMap="LoginInfoResultMap">
        SELECT account_user.id,
               account_user.username,
               account_user.password,
               account_user.nickname,
               account_user.avatar,
               account_user.enable,
               account_permission.value
        FROM account_user
                 LEFT JOIN account_user_role ON account_user.id = account_user_role.user_id
                 LEFT JOIN account_role_permission ON account_user_role.role_id = account_role_permission.role_id
                 left join account_permission on account_role_permission.permission_id = account_permission.id
        where username = #{username}
    </select>

    <resultMap id="LoginInfoResultMap" type="cn.ylz.ylmall.account.pojo.vo.UserLoginInfoVO">
        <id column="id" property="id"/>
        <result column="username" property="username"/>
        <result column="password" property="password"/>
        <result column="nickname" property="nickname"/>
        <result column="avatar" property="avatar"/>
        <result column="enable" property="enable"/>
        <collection property="permissions" ofType="java.lang.String">
            <constructor>
                <arg column="value"/>
            </constructor>
        </collection>
    </resultMap>

2.5 controller以及service层接口处理登录的方法

判断权限在controller方法上加上

@PreAuthorize("hasAuthority('/account/user/update')")

即可

	@PostMapping("login")
    @ApiOperation(value = "用户登录")
    @ApiOperationSupport(order = 1)
    public JsonResult login(@Validated UserLoginParam userLoginParam,HttpServletRequest req, HttpServletResponse resp) {
        UserLoginResultVO loginResultVO = userService.login(userLoginParam,req,resp);
        return JsonResult.ok(loginResultVO);
    }

UserService接口:UserLoginResultVO login(UserLoginParam userLoginParam);

这里在登录成功后不单单只返回token,还有ID,用户名,头像等信息。

UserLoginParam类里面就是用户输入的username和password

注意:向JWT中存入权限时,应先将权限列表对象(ArrayList)转成JSON字符串,解析时再指定转换类型,GrantedAuthority.
若直接存入权限列表对象(ArrayList),解析时元素是一个LinkedHashMap.而不是权限类型GrantedAuthority.

2.6 UserServiceI实现类

UserServiceImpl类:

    @Value("${ylmall.jwt.secret-key}")
    private String secretKey;

    @Value("${ylmall.jwt.duration-in-minute}")
    private long durationInMinute;
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private IUserRepository userRepository;
    @Autowired
    private IUserRoleRepository userRoleRepository;
    @Autowired
    private IUserCacheRepository cacheRepository;

    @Override
    public UserLoginResultVO login(UserLoginParam userLoginParam, HttpServletRequest req, HttpServletResponse resp) {
        log.debug("用户登录信息, {}", userLoginParam);
        Authentication authentication =
                    new UsernamePasswordAuthenticationToken(userLoginParam.getUsername(), userLoginParam.getPassword());
            Authentication authenticateResult = authenticationManager.authenticate(authentication);
            log.debug("验证用户登录成功,返回的认证结果:{}", authenticateResult);
            Object principal = authenticateResult.getPrincipal();
            log.debug("从认证结果中获取当事人:{}", principal);
            CustomUserDetails userDetails = (CustomUserDetails) principal;
            log.debug("准备将认证信息结果存入到SecurityContext中……");
            SecurityContext securityContext = SecurityContextHolder.getContext();
            securityContext.setAuthentication(authenticateResult);
            log.debug("已经将认证信息存入到SecurityContext中,登录业务处理完成!");
            Collection<GrantedAuthority> authorities = userDetails.getAuthorities();
            String authoritiesJsonString = JSON.toJSONString(authorities);
            log.debug(authoritiesJsonString);
            Long id = userDetails.getId();
            String username = userDetails.getUsername();
            String avatar = userDetails.getAvatar();
            Map<String, Object> claims = new HashMap<>();
            claims.put("id", id);
            claims.put("username", username);
            // claims.put("authoritiesJsonString", authoritiesJsonString);
            Date date = new Date(System.currentTimeMillis() + 1L * durationInMinute * 60 * 1000);
            String jwt = Jwts.builder()
                    // Header
                    .setHeaderParam("alg", "HS256")
                    .setHeaderParam("typ", "JWT")
                    // Payload
                    .setClaims(claims)
                    .setExpiration(date)
                    // Verify Signature
                    .signWith(SignatureAlgorithm.HS256, secretKey)
                    // Done
                    .compact();
            // 将权限列表存入Redis
            UserLoginInfoPO userLoginInfoPO = new UserLoginInfoPO();
            userLoginInfoPO.setIp(req.getRemoteAddr());
            userLoginInfoPO.setUserAgent(req.getHeader(HEADER_USER_AGENT));
            userLoginInfoPO.setAuthoritiesJsonString(authoritiesJsonString);
            cacheRepository.saveLoginInfo(jwt, userLoginInfoPO);
            // 将用户状态存入Redis
            cacheRepository.saveEnableByUserId(id, 1);
            UserLoginResultVO userLoginResultVO = new UserLoginResultVO().setId(id).setUsername(username).setAvatar(avatar).setToken(jwt);
            return userLoginResultVO;
    }
  1. 客户端传递用户名和密码参数给 Spring Security 提供的 UsernamePasswordAuthenticationToken 类,该类表示用户名和密码的认证对象。这个类将作为认证的输入。
  2. 调用 authenticationManager 的 authenticate 方法,对认证对象进行验证,判断用户名和密码是否有效。该方法返回一个 Authentication 类型的认证结果对象 authenticateResult。
  3. 从认证结果对象中获取当事人(principal),即经过认证的用户对象。
  4. 将当事人对象转换为 CustomUserDetails 类型,这是一个自定义的用户详细信息类,包含了用户的额外信息,如ID、头像等。
  5. 从认证对象中获取相关信息,例如用户ID、用户名、头像和权限列表等。
  6. 创建一个 JWT(JSON Web Token)构建器,设置头部参数、声明信息、过期时间、签名等信息,并使用 compact() 方法生成 JWT 字符串。JWT 是一种用于进行身份验证和授权的标准化令牌。
  7. 创建一个 UserLoginInfoPO 对象,将用户权限列表转换为 JSON 格式的字符串,并将其存储到 Redis 中。
  8. 将用户登录状态存储到 Redis 中,通过用户ID和状态值进行存储。
  9. 返回用户登录的结果信息,例如用户ID、用户名、头像和生成的 JWT 令牌等。

此时就可以正常登录,并响应token

在这里插入图片描述

然后再knife4j的接口文档中全局参数设置中配置请求头信息,参数值为上面响应的token.

在这里插入图片描述

2.7 登录失败异常处理

若登录失败,全局异常处理器处理异常(登录失败(用户名不存在或密码错误),用户被禁用,权限不足)

@ExceptionHandler({
            InternalAuthenticationServiceException.class,
            BadCredentialsException.class
    })
    public JsonResult handleAuthenticationException(AuthenticationException e) {
        log.debug("全局异常处理器开始处理AuthenticationException");
        log.debug("异常类型:{}", e.getClass().getName());
        String message = "登录失败,用户名或密码错误!";
        return JsonResult.fail(ServiceCode.ERROR_UNAUTHORIZED, message);
    }

    @ExceptionHandler
    public JsonResult handleDisabledException(DisabledException e) {
        log.debug("全局异常处理器开始处理DisabledException");
        String message = "登录失败,此账号已经被禁用!";
        return JsonResult.fail(ServiceCode.ERROR_UNAUTHORIZED_DISABLED, message);
    }

    @ExceptionHandler
    public JsonResult handleAccessDeniedException(AccessDeniedException e) {
        log.debug("全局异常处理器开始处理AccessDeniedException");
        String message = "操作失败,当前登录的账号无此操作权限!";
        return JsonResult.fail(ServiceCode.ERROR_FORBIDDEN, message);
    }

此时,我们的权限就已经设置好了,当用户不登录直接访问需要授权才能访问的资源时提示用户当前未登录,在上面SecurityConfiguration中已经写好了。

使用到的请求头直接定义在HttpConsts接口中

public interface HttpConsts {

    /**
     * 请求头:授权信息
     */
    String HEADER_AUTHORIZATION = "Authorization";

    /**
     * 请求头:客户端浏览器
     */
    String HEADER_USER_AGENT = "User-Agent";

}

2.8 添加过滤器来解析并验证jwt

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

设计当事人类型,处理请求时知道当前操作的用户信息,例如id或username,可在处理请求的方法参数中通过@AuthenticationPrincipal CurrentPrincipal currentPrincipal,通过get方法获取.

@Data
public class CurrentPrincipal implements Serializable {

    /**
     * 当事人ID
     */
    private Long id;
    /**
     * 当事人用户名
     */
    private String username;

}
@Component
@Slf4j
public class JwtAuthorizationFilter extends OncePerRequestFilter implements HttpConsts {
    @Value("${ylmall.jwt.secret-key}")
    private String secretKey;
    @Autowired
    private IUserCacheRepository userCacheRepository;
    public static final int JWT_MIN_LENGTH = 113;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        log.debug("处理JWT的过滤器开始处理当前请求……");
        String jwt = request.getHeader(HEADER_AUTHORIZATION);
        log.debug("客户端携带的JWT:{}", jwt);

        // 判断jwt是否有效
        if (!StringUtils.hasText(jwt) || jwt.length() < JWT_MIN_LENGTH) {
            // 对于无效的JWT,应该直接放行
            log.warn("当前请求中,客户端没有携带有效的JWT,将放行");
            filterChain.doFilter(request, response);
            return;
        }

        UserLoginInfoPO loginInfo = userCacheRepository.getLoginInfo(jwt);
        if (loginInfo == null) {
            // 放行,不会向SecurityContext中存入认证信息,则相当于没有携带JWT
            log.warn("在Redis中无此JWT对应的信息,将直接放行,交由后续的组件进行处理");
            filterChain.doFilter(request, response);
            return;
        }
        String ip = request.getRemoteAddr();
        String userAgent = request.getHeader(HEADER_USER_AGENT);
        if (!ip.equals(loginInfo.getIp()) && !userAgent.equals(loginInfo.getUserAgent())) {
            log.warn("本次请求的信息与当初登录时完全不同,将直接放行,交由后续的组件进行处理");
            filterChain.doFilter(request, response);
            return;
        }

        // 解析jwt
        log.debug("尝试解析JWT数据……");
        response.setContentType("application/json; charset=utf-8");
        Claims claims = null;
        try {
            claims = Jwts.parser()
                    .setSigningKey(secretKey)
                    .parseClaimsJws(jwt)
                    .getBody();
        } catch (ExpiredJwtException e) {
            log.warn("解析JWT时出现异常:ExpiredJwtException");
            String message = "操作失败,您的登录信息已经过期,请重新登录!";
            JsonResult jsonResult = JsonResult.fail(ServiceCode.ERR_JWT_EXPIRED, message);
            PrintWriter writer = response.getWriter();
            writer.println(JSON.toJSONString(jsonResult));
            writer.close();
            return;
        } catch (SignatureException e) {
            log.warn("解析JWT时出现异常:SignatureException");
            String message = "非法访问,你的本次操作已经被记录!";
            JsonResult jsonResult = JsonResult.fail(ServiceCode.ERR_JWT_SIGNATURE, message);
            PrintWriter writer = response.getWriter();
            writer.println(JSON.toJSONString(jsonResult));
            writer.close();
            return;
        } catch (MalformedJwtException e) {
            log.warn("解析JWT时出现异常:MalformedJwtException");
            String message = "非法访问,你的本次操作已经被记录!";
            JsonResult jsonResult = JsonResult.fail(ServiceCode.ERR_JWT_MALFORMED, message);
            PrintWriter writer = response.getWriter();
            writer.println(JSON.toJSONString(jsonResult));
            writer.close();
            return;
        } catch (Throwable e) {
            log.warn("解析JWT时出现异常:{}", e);
            String message = "检查服务器端的控制台";
            JsonResult jsonResult = JsonResult.fail(ServiceCode.ERROR_UNKNOWN, message);
            PrintWriter writer = response.getWriter();
            writer.println(JSON.toJSONString(jsonResult));
            writer.close();
            return;
        }

        Long id = claims.get("id", Long.class);
        String username = claims.get("username", String.class);

        // 凭证:无需凭证
        Object credentials = null;
        // 将解析得到的用户数据创建为Authentication对象
        CurrentPrincipal principal = new CurrentPrincipal();
        principal.setId(id);
        principal.setUsername(username);

        // 检查用户的启用状态
        Integer userEnable = userCacheRepository.getEnableByUserId(id);
        if (userEnable != 1) {
            log.warn("用户已被禁用");
            String message = "用户已被禁用";
            JsonResult jsonResult = JsonResult.fail(ServiceCode.ERROR_UNAUTHORIZED_DISABLED, message);
            PrintWriter writer = response.getWriter();
            writer.println(JSON.toJSONString(jsonResult));
            writer.close();
            return;
        }

        // 权限列表从Redis中读取
        String authoritiesJsonString = loginInfo.getAuthoritiesJsonString();
        List<SimpleGrantedAuthority> authorityList = JSON.parseArray(authoritiesJsonString, SimpleGrantedAuthority.class);
        Authentication authentication = new UsernamePasswordAuthenticationToken(principal, credentials, authorityList);

        // 将Authentication对象存入到SecurityContext中
        SecurityContext securityContext = SecurityContextHolder.getContext();
        securityContext.setAuthentication(authentication);

        // 过滤器链继续执行,即:放行
        filterChain.doFilter(request, response);

    }
}

2.9 使用Redis保存权限列表及用户状态

如果当前登录的用户权限很多,登录成功返回的jwt就会非常长,以及考虑到jwt中不易放敏感信息,我们使用Redis来保存和读取用户的权限列表。

  • 还有避免token被盗用的问题,在客户端响应JWT时,此JWT作为Redis中Key值的一部分,在value中存入客户端登录时的浏览器信息和IP地址,并且,在过滤器中接收到JWT后,就立即检查Redis中此JWT对应的浏览器信息与IP地址,与当前客户端的浏览器、IP地址进行比对,如果一项是符合的,视为合法否则视为盗用的JWT。
  • 以及避免Redis中的JWT数据过多,在存入时根据JWT的有效期设置Redis中对应数据的有效期。
  • 为了更好的控制用户的状态,把用户状态也存入到Redis,如果管理员将某个用户禁用,并且在Redis中存在此用户的JWT,会把Redis中的用户状态也设置为禁用,在过滤器中处理JWT时,也会检查Redis中用户的状态,如果被禁用,将向客户端响应错误信息。
public interface UserCacheConsts {

    /**
     * 缓存的JWT前缀
     */
    String USER_JWT_PREFIX = "user:jwt:";
    /**
     * 用户启用状态的KEY的前缀
     */
    String USER_ENABLE_PREFIX = "user:enable:";

}

public interface IUserCacheRepository extends UserCacheConsts {
    void saveLoginInfo(String jwt, UserLoginInfoPO userLoginInfoPO);
    UserLoginInfoPO getLoginInfo(String jwt);
    void saveEnableByUserId(Long userId,Integer enable);
    Integer getEnableByUserId(Long userId);
}
@Repository
public class UserCacheRepositoryImpl implements IUserCacheRepository {
    @Value("${ylmall.jwt.duration-in-minute}")
    private long durationInMinute;

    @Autowired
    private RedisTemplate<String, Serializable> redisTemplate;


    @Override
    public void saveLoginInfo(String jwt, UserLoginInfoPO userLoginInfoPO) {
        String key = USER_JWT_PREFIX+jwt;
        ValueOperations<String, Serializable> operations = redisTemplate.opsForValue();
        operations.set(key, (Serializable) userLoginInfoPO,durationInMinute, TimeUnit.MINUTES);
    }

    @Override
    public UserLoginInfoPO getLoginInfo(String jwt) {
        String key = USER_JWT_PREFIX+jwt;
        ValueOperations<String, Serializable> operations = redisTemplate.opsForValue();
        return (UserLoginInfoPO) operations.get(key);
    }

    @Override
    public void saveEnableByUserId(Long userId, Integer enable) {
        String key = USER_ENABLE_PREFIX+userId;
        redisTemplate.opsForValue().set(key,enable,durationInMinute,TimeUnit.MINUTES);
    }

    @Override
    public Integer getEnableByUserId(Long userId) {
        String key = USER_ENABLE_PREFIX+userId;
        return (Integer) redisTemplate.opsForValue().get(key);
    }
}

此时,我们需要再用户登录成功时,将权限列表以及用户状态存入Redis(在UserServiceImpl类中已经写好),在过滤器中,先从Redis中读取用户状态进行验证,若用户被禁用,向客户端返回错误信息,以及从Redis中读取用户权限,得到的用户数据创建为Authentication对象,再将Authentication对象存入到SecurityContext中。

3.前端

登录成功拿到响应的jwt存入localStorage,在发请求时携带jwt。

let loginResult = jsonResult.data;
localStorage.setItem('localJwt', loginResult.token);
this.axios
    .create({'headers': {'Authorization': localStorage.getItem('localJwt')}})
    .post(url).then((response) => {}

写得不好还请各位大佬指正一下!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Spring Boot 是一个用于构建微服务的开源框架,它能够快速搭建项目并且提供了许多便捷的功能和特性。Spring Security 是一个用于处理认证和授权的框架,可以保护我们的应用程序免受恶意攻击。JWT(JSON Web Token)是一种用于身份验证的开放标准,可以被用于安全地传输信息。Spring MVC 是一个用于构建 Web 应用程序的框架,它能够处理 HTTP 请求和响应。MyBatis 是一个用于操作数据库的框架,可以简化数据库操作和提高效率。Redis 是一种高性能的键值存储系统,可以用于缓存与数据存储。 基于这些技术,可以搭建一个商城项目。Spring Boot 可以用于构建商城项目的后端服务,Spring Security 可以确保用户信息的安全性,JWT 可以用于用户的身份验证Spring MVC 可以处理前端请求,MyBatis 可以操作数据库,Redis 可以用于缓存用户信息和商品信息。 商城项目的后端可以使用 Spring BootSpring Security 来搭建,通过 JWT 来处理用户的身份验证授权。数据库操作可以使用 MyBatis 来简化与提高效率,同时可以利用 Redis 来缓存一些常用的数据和信息,提升系统的性能。前端请求则可以通过 Spring MVC 来处理,实现商城项目的整体功能。 综上所述,借助于 Spring BootSpring SecurityJWTSpring MVC、MyBatis 和 Redis 这些技术,可以构建出一个高性能、安全可靠的商城项目,为用户提供良好的购物体验。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

学抓哇的小杨

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值