Spring Security学习笔记(三)Spring Security+JWT认证授权流程代码实例

news2024/9/9 4:13:40

前言:本系列博客基于Spring Boot 2.6.x依赖的Spring Security5.6.x版本

上两篇文章介绍了Spring Security的整体架构以及认证和鉴权模块原理。本篇文章就是基于Spring Security和JWT的一个demo

一、JWT简介

JWT(JSON Web Token),是一种开放标准(RFC 7519),用于在网络应用环境间安全地传输信息。本质上是一个经过数字签名的JSON对象,能够携带并传递状态信息(如用户身份验证、授权等)

1.1、JWT的结构

JWT由三部分组成,通过点号(.)连接,这三部分分别是头部(Header)、载荷(Payload)和签名(Signature)。类似于xxxx.xxxx.xxxx格式。如下:

eyJhbGciOiJIUzUxMiJ9.eyJMT0dJTl9USU1FIjoxNzIyMzEzMDg4NTU4LCJMT0dJTl9VU0VSIjoidXNlcjIiLCJleHAiOjE3MjIzMTY2ODh9.l-mw4sWCWvIrWSRHUPdiLlgH6tIFxbwx7KwUj0Ldf4CDbdOqQlDuj-x0y6zM4R84vmnRLBBDeH_oLRxx0rcNxQ
  • Header:头部,声明了JWT的类型(通常是JWT)以及所使用的加密算法(例如HMAC SHA256或RSA)
  • Payload:载荷,承载实际数据的部分,可以包含预定义的声明(如iss(签发者)、exp(过期时间)、sub(主题)等)以及其它自定义的数据。这些信息都是铭文的,但不建议存放敏感信息。
  • Signature:签名,通过对前两部分进行编码后的信息,使用指定的密钥通过头部(Header)中声明的加密算法生成,拥有验证数据完整性和防止篡改。

这三部分单独使用base64编码后再通过点号(.)连接。

这里只简单介绍JWT,如果需要详细了解JWT的可以参考以下文章

https://blog.csdn.net/weixin_42753193/article/details/126294904
https://www.cnblogs.com/moonlightL/p/10020732.html
JWT官网

二、Spring Security+JWT认证授权流程代码代码实例

2.1、新建Springboot项目,引入JAR包

新建好Springboot项目,引入用到的jar包
pom文件(只写出了dependencies):

	<!--Springboot父工程,定义好了Springboot集成的其他jar包版本,所以引入某些jar时可以不写版本号-->
	<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.15</version>
    </parent>
    
	<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                </exclusion>
            </exclusions>
        </dependency>


        <!--使用undertow容器-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-undertow</artifactId>
        </dependency>

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

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

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

        <!--自定义配置生成元数据信息,这样在配置文件中可以有提示-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

        <!--Spring Security-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <!--mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!--mybatis-plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>

        <!-- JSON Web Token Support -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
        </dependency>

        <!--redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

    </dependencies>

application.yaml配置文件:

server:
  port: 8084
  servlet:
    context-path: /security

mybatis-plus:
  mapper-locations: classpath*:mapper/**/*Mapper.xml
  # 使用驼峰命名
  # 数据库表列:user_name
  # 实体类属性:userName
  configuration:
    map-underscore-to-camel-case: true

Spring:
  redis:
    host: 127.0.0.1
    port: 6379
    lettuce:
      pool:
        max-idle: 16
        max-active: 32
        min-idle: 8
  datasource:
    # 数据源基本配置
    username: root
    password: root1234
    url: jdbc:mysql://127.0.0.1:3306/test?allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
    # driver-class需要注意mysql驱动的版本(com.mysql.cj.jdbc.Driver 或 com.mysql.jdbc.Driver)
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.zaxxer.hikari.HikariDataSource
    hikari:
      pool-name: Retail_HikariCP
      minimum-idle: 5 #最小空闲连接数
      idle-timeout: 180000 #空闲连接存活最长时间 默认600000(10分钟)
      maximum-pool-size: 10 #连接池最大连接数,默认10
      auto-commit: true #此属性控制从连接池返回的连接的默认自动提交行为,默认true
      max-lifetime: 1800000 #连接的最长生命周期,0表示无限,默认1800000即30分钟
      connection-timeout: 30000 #数据库连接超时时间,默认30秒,即3000
      connection-test-query: SELECT 1 FROM DUAL

2.2、数据库操作相关类

数据库脚本(mysql):

create table `manager`(
	`id` int NOT NULL AUTO_INCREMENT,
	`login_name` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '登录名',
	`password` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '密码',
	`name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '姓名',
	`id_number` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '身份证',
	`mobile` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '手机号',
	`email` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '邮箱',
	PRIMARY KEY (`id`) USING BTREE
)ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic COMMENT = '管理员表';
##密码是123456
INSERT INTO `manager` (`id`, `login_name`, `password`, `name`, `id_number`, `mobile`, `email`) VALUES (1, 'user1', '$2a$10$JrdOPx3zKcNqLQnU7GrdUeE2XA3KXZgu3QqLCeBTJWPxJjOOfOHGG', '张三', NULL, NULL, NULL);
INSERT INTO `manager` (`id`, `login_name`, `password`, `name`, `id_number`, `mobile`, `email`) VALUES (2, 'user2', '$2a$10$JrdOPx3zKcNqLQnU7GrdUeE2XA3KXZgu3QqLCeBTJWPxJjOOfOHGG', '李四', NULL, NULL, NULL);

create table `role`(
	`id` int NOT NULL AUTO_INCREMENT,
	`name` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '角色名',
	`code` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '角色编码',
	`type` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '角色类别',
	`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '角色描述',
	PRIMARY KEY (`id`) USING BTREE
)ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic COMMENT = '角色表';

INSERT INTO `role` (`id`, `name`, `code`, `type`, `description`) VALUES (1, '管理员角色', 'AdminManager', 'admin', NULL);
INSERT INTO `role` (`id`, `name`, `code`, `type`, `description`) VALUES (2, '审批用户角色', 'ApproveUser', 'approve', NULL);

create table `permission`(
	`id` int NOT NULL AUTO_INCREMENT,
	`name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '权限名',
	`code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '权限编码',
	`type` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '权限类别',
	`url` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '资源权限路径',
	`anonymous` int NOT NULL COMMENT '是否可以匿名访问 1-是 0-否',
	`description` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '权限描述',
	PRIMARY KEY (`id`) USING BTREE
)ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic COMMENT = '权限表';

INSERT INTO `permission` (`id`, `name`, `code`, `type`, `url`, `description`, `anonymous`) VALUES (1, '主页接口', 'main', 'interface', '/main', NULL, 0);
INSERT INTO `permission` (`id`, `name`, `code`, `type`, `url`, `description`, `anonymous`) VALUES (2, '测试接口1', 'test1', 'interface', '/adminRole', NULL, 0);
INSERT INTO `permission` (`id`, `name`, `code`, `type`, `url`, `description`, `anonymous`) VALUES (3, '测试接口2', 'test2', 'interface', '/touristRole', NULL, 0);
INSERT INTO `permission` (`id`, `name`, `code`, `type`, `url`, `description`, `anonymous`) VALUES (4, '登录接口', 'login', 'interface', '/login', NULL, 1);
INSERT INTO `permission` (`id`, `name`, `code`, `type`, `url`, `description`, `anonymous`) VALUES (5, '注销接口', 'logout', 'interface', '/myLogout', NULL, 1);

create table `manager_role_rel`(
	`id` int NOT NULL AUTO_INCREMENT,
	`manager_id` int NOT NULL COMMENT '用户id',
	`role_id` int NOT NULL COMMENT '角色id',
	PRIMARY KEY (`id`) USING BTREE
)ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic COMMENT = '用户角色关联表';

INSERT INTO `manager_role_rel` (`id`, `manager_id`, `role_id`) VALUES (1, 1, 1);
INSERT INTO `manager_role_rel` (`id`, `manager_id`, `role_id`) VALUES (2, 2, 2);
INSERT INTO `manager_role_rel` (`id`, `manager_id`, `role_id`) VALUES (3, 1, 2);

create table `role_permission_rel`(
	`id` int NOT NULL AUTO_INCREMENT,
	`role_id` int NOT NULL COMMENT '用户id',
	`permission_id` int NOT NULL COMMENT '角色id',
	PRIMARY KEY (`id`) USING BTREE
)ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic COMMENT = '角色权限关联表';

INSERT INTO `role_permission_rel` (`id`, `role_id`, `permission_id`) VALUES (1, 1, 1);
INSERT INTO `role_permission_rel` (`id`, `role_id`, `permission_id`) VALUES (2, 1, 2);
INSERT INTO `role_permission_rel` (`id`, `role_id`, `permission_id`) VALUES (3, 1, 3);
INSERT INTO `role_permission_rel` (`id`, `role_id`, `permission_id`) VALUES (4, 2, 1);
INSERT INTO `role_permission_rel` (`id`, `role_id`, `permission_id`) VALUES (5, 2, 3);

实体类:

@Data
@TableName("manager")
public class ManagerDomain {
    @TableId(type = IdType.AUTO)
    private Integer id;
    //@TableField("user_name")
    private String loginName;
    private String password;
    private String name;
    private String idNumber;
    private String mobile;
    private String email;
}

@Data
@TableName("permission")
public class PermissionDomain {
    @TableId(type = IdType.AUTO)
    private Integer id;
    private String name;
    private String code;
    private String type;
    private String url;
    private String description;
    private Integer anonymous;
}

@Data
@TableName("role")
public class RoleDomain {
    @TableId(type = IdType.AUTO)
    private Integer id;
    private String name;
    private String code;
    private String description;
}

mybatis的Mapper接口及配置文件:

@Mapper
public interface ManagerMapper extends BaseMapper<ManagerDomain> {
}

@Mapper
public interface PermissionMapper extends BaseMapper<PermissionDomain> {
    /**
     * 根据角色code获取该角色的资源权限url
     * @param roleCode
     * @return
     */
    List<String> getPermissionUrlByRole(String roleCode);

    List<String> getAnonymousPermissionUrl();
}

@Mapper
public interface RoleMapper extends BaseMapper<RoleDomain> {
    /**
     * 根据用户id获取该用户拥有的角色的code
     * @param managerId
     * @return
     */
    List<String> getRoleCodeByManagerId(Integer managerId);

    /**
     * 获取所有角色的code
     * @return
     */
    List<String> getAllRoleCode();
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.dmf.demo.jwt.security.dao.ManagerMapper">

</mapper>

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.dmf.demo.jwt.security.dao.PermissionMapper">
    <select id="getPermissionUrlByRole" resultType="java.lang.String">
        select p.url
            from role r
        LEFT JOIN role_permission_rel rpr on r.id = rpr.role_id
        left join permission p on rpr.permission_id = p.id
        where r.code = #{roleCode}
    </select>
    <select id="getAnonymousPermissionUrl" resultType="java.lang.String">
        select url from permission where anonymous = 1
    </select>
</mapper>

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.dmf.demo.jwt.security.dao.RoleMapper">

    <select id="getRoleCodeByManagerId" resultType="java.lang.String">
        select r.code
            from role r
        left join manager_role_rel mrr on mrr.role_id = r.id
        WHERE mrr.manager_id= #{managerId}
    </select>
    <select id="getAllRoleCode" resultType="java.lang.String">
        select code from role
    </select>
</mapper>

2.3、Controller和Service

@Slf4j
@Controller
public class SystemController {
    @Autowired
    private SystemService systemService;

    /**
     * 登录
     * @param userName
     * @param password
     * @return
     */
    @RequestMapping("/login")
    @ResponseBody
    public String login(String userName, String password){
        log.info("用户{}登录",userName);
        return systemService.login(userName,password);
    }

	@RequestMapping("/myLogout")
    @ResponseBody
    public String logout(HttpServletRequest request){
        systemService.logout(request);
        return "success";
    }
    
    /**
     * @return
     */
    @RequestMapping("/adminRole")
    @ResponseBody
    public String adminRole(){
        return "success";
    }

    @RequestMapping("/touristRole")
    @ResponseBody
    public String touristRole(){
        return "success";
    }
}

service接口及实现类

public interface SystemService {
    String login(String userName,String password);
    void logout(HttpServletRequest request);
}

SystemService 实现类:

@Slf4j
@Service
public class SystemServiceImpl implements SystemService {

    @Resource
    private AuthenticationManager authenticationManager;
    @Resource
    private RedisTemplate<String,String> stringRedisTemplate;

    @Override
    public String login(String userName, String password) {
        //1、根据用户输入的用户名和密码创建认证凭证Authentication
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(userName, password);
        //2、调用AuthenticationManager认证管理器的authenticate方法进行认证操作,返回认证成功后的凭证Authentication
        Authentication authenticate = null;
        try {
            authenticate = authenticationManager.authenticate(authenticationToken);
        } catch (AuthenticationException e) {
            //这里自己捕获认证异常,自己处理,如果自己不处理的话,异常会交给自定义的AuthenticationEntryPoint处理
            //如果没定义AuthenticationEntryPoint,Spring Security会默认返回403
            log.error("登录失败!原因:{}",e.getMessage());
            throw new RuntimeException("登录失败!");
        }
        //3、生成jwt
        //拿到认证成功后的用户信息
        LoginUserDetails userDetails = (LoginUserDetails) authenticate.getPrincipal();
        String accessToken = JwtUtils.createToken(userDetails);

        //4、保存用户信息到redis
        LoginUserInfoDto loginUserInfoDto = LoginUserInfoDto.builder()
                .loginName(userDetails.getUsername())
                .id(userDetails.getManager().getId())
                .name(userDetails.getManager().getName())
                .mobile(userDetails.getManager().getMobile())
                .roles(userDetails.getRoles())
                .build();
        String key = GlobalConstants.LOGIN_CACHE_KEY_PREFIX+userDetails.getUsername();

        stringRedisTemplate.opsForValue().set(key, JSONObject.toJSONString(loginUserInfoDto),60, TimeUnit.MINUTES);
        return accessToken;
    }

    @Override
    public void logout(HttpServletRequest request) {
        String token = request.getHeader("token");
        if(StringUtils.isNotEmpty(token)){
            String userName = JwtUtils.getUserName(token);
            //清除redis
            if(StringUtils.isNotEmpty(userName)){
                String key = GlobalConstants.LOGIN_CACHE_KEY_PREFIX+userName;
                stringRedisTemplate.delete(key);
            }
        }
    }
}

用户登录信息实体类LoginUserInfoDto:

@Data
@Builder
public class LoginUserInfoDto {
    private Integer id;
    private String loginName;
    private String name;
    private String idNumber;
    private String mobile;
    private List<String> roles;

    /**
     * 组装spring security的权限
     * @return
     */
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<SimpleGrantedAuthority> grantedAuthorities = new ArrayList<>();
        if(!CollectionUtils.isEmpty(roles)){
            roles.forEach(roleCode ->{
                grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + roleCode));
            });
        }
        return grantedAuthorities;
    }
}

全局常数类:

public class GlobalConstants {
    /**
     * 请求携带的token参数,参数名
     */
    public static final String HEADER_TOKEN_NAME = "token";
    /**
     * 用户登录信息缓存KEY前缀
     */
    public static final String LOGIN_CACHE_KEY_PREFIX = "USER_INFO:";
    /**
     * 全局资源权限缓存key
     */
    public static final String GLOBAL_PERMISSION_KEY_PREFIX = "GLOBAL_PERMISSION:";
    /**
     * 允许匿名访问资源缓存key
     */
    public static final String GLOBAL_PERMISSION_ANONYMOUS = "GLOBAL_PERMISSION:ANONYMOUS";
}

JWT工具类:

@Slf4j
public class JwtUtils {
    /** jwt加密秘钥*/
    public static final String DEFAULT_SECRET = "abcdefghijk";
    /** jwt数据声明里登录用户key*/
    public static final String LOGIN_USER = "LOGIN_USER";
    /** jwt数据声明里登录时间key*/
    public static final String LOGIN_TIME = "LOGIN_TIME";
    /** jwt默认过期时间*/
    public static Long DEFAULT_TTL = 60*60*1000l; //一个小时


    /**
     * 生成jwt使用默认设置
     * @param claims
     * @return
     */
    public static String createToken(Map<String, Object> claims){
        return createToken(claims,DEFAULT_TTL,DEFAULT_SECRET);
    }

    /**
     * 生成jwt
     * @param claims
     * @param ttl 过期时间 ms
     * @return
     */
    public static String createToken(Map<String, Object> claims,Long ttl){
        return createToken(claims,ttl,DEFAULT_SECRET);
    }

    /**
     *
     * @param userDetails Spring Security用户信息
     * @param ttl 过期时间 ms
     * @return
     */
    public static String createToken(UserDetails userDetails,Long ttl){
        Map<String, Object> claims = new HashMap<>();
        claims.put(LOGIN_USER,userDetails.getUsername());
        claims.put(LOGIN_TIME,new Date());

        return createToken(claims,ttl,DEFAULT_SECRET);
    }

    /**
     *
     * @param userDetails
     * @return
     */
    public static String createToken(UserDetails userDetails){
        return createToken(userDetails,DEFAULT_TTL);
    }

    /**
     * 生成jwt
     * @param claims
     * @return
     */
    public static String createToken(Map<String, Object> claims,Long ttl,String secret){
        return Jwts.builder()
                .setClaims(claims)  //设置数据
                .setExpiration(generateExpirationDate(ttl))
                .signWith(SignatureAlgorithm.HS512, secret) //签名,参数包括算法和秘钥
                .compact(); //压缩生成xxx.xxx.xxx
    }

    /**
     * 生成token的过期时间
     * @param ttl 单位是毫秒
     * @return
     */
    private static Date generateExpirationDate(Long ttl) {
        return new Date(System.currentTimeMillis() + ttl);
    }

    /**
     * 解析jwt拿到数据,使用默认配置
     * @param token
     * @return
     */
    public static Claims parseToken(String token){
        return  parseToken(token,DEFAULT_SECRET);
    }

    /**
     * 解析jwt拿到数据
     * @param token
     * @return
     */
    public static Claims parseToken(String token,String secret){
        Claims claims = Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();
        return claims;
    }

    /**
     * 获取jwt里的用户名称
     * @param token
     * @return
     */
    public static String getUserName(String token){
        return (String)parseToken(token).get(LOGIN_USER);
    }

    /**
     * token是否已经过期
     * @param claims
     * @return
     */
    private static boolean isTokenExpired(Claims claims) {
        Date expire = claims.getExpiration();
        if(expire!=null){
            return expire.before(new Date());
        }
        return false;
    }
}

2.4、Spring Security自定义认证和鉴权

上篇文章已经介绍过Spring Security的认证和鉴权架构。
认证:
Spring Security的认证主要由AuthenticationManager -> AuthenticationProvider流程。而AuthenticationProvider调用UserDetailsService的loadUserByUsername方法先查询系统用户,再和用户输入的用户信息做比对认证。
所以自定义认证,我们只需要在配置类里定义自己的AuthenticationManager和AuthenticationProvider,以及实现UserDetailsService接口。另外UserDetails类的默认实现类User使用不方便,也可以实现自定义的UserDetails来做功能扩展

自定义的UserDetails实现类

@Data
@Builder
public class LoginUserDetails implements UserDetails {

    private ManagerDomain manager;
    private Integer id;
    private String username;
    private String password;
    private boolean enabled;
    private boolean locked;
    private Collection<? extends GrantedAuthority> grantedAuthorities;

    private List<String> roles;

    public Integer getUserId() {
        return this.manager.getId();
    }

    // 返回当前用户的权限列表
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if (grantedAuthorities != null)
            return this.grantedAuthorities;
        List<SimpleGrantedAuthority> grantedAuthorities = new ArrayList<>();
        if(!CollectionUtils.isEmpty(roles)){
            roles.forEach(roleCode ->{
                grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + roleCode));
            });
        }
        return grantedAuthorities;
    }

    @Override
    public String getPassword() {
        return this.password;
    }
    @Override
    public String getUsername() {
        return this.username;
    }
    //账号是否未过期,直接返回true 表示账户未过期,也可以在数据库中添加该字段
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    //账号是否被锁, 这里和数据库中的locked字段刚好相反,所有取反
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    //密码是否为过期,数据库中无该字段,直接返回true
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    //账户是否可用,从数据库中获取该字段
    @Override
    public boolean isEnabled() {
        return true;
    }

}

自定义的UserDetailsService实现类:

public class DBUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {

    @Resource
    private ManagerMapper managerMapper;
    @Resource
    private RoleMapper roleMapper;

    /**
     * UserDetails提供的字段如果不够的话,可以继承 User类,实现自己的UserDetails
     * 用户认证时会调用
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        ManagerDomain user = managerMapper.selectOne(new LambdaQueryWrapper<ManagerDomain>()
                .eq(ManagerDomain::getLoginName,username)
        );
        if(user == null){
            throw new UsernameNotFoundException(username);
        }
        //查询用户角色
        List<String> roles = roleMapper.getRoleCodeByManagerId(user.getId());
        LoginUserDetails userDetails = LoginUserDetails.builder()
                .username(user.getLoginName())
                .password(user.getPassword())
                .manager(user)
                .roles(roles)
                .build();
        return userDetails;
    }

    @Override
    public void createUser(UserDetails user) {

    }

    @Override
    public void updateUser(UserDetails user) {

    }

    @Override
    public void deleteUser(String username) {

    }

    @Override
    public void changePassword(String oldPassword, String newPassword) {

    }

    @Override
    public boolean userExists(String username) {
        return false;
    }

    @Override
    public UserDetails updatePassword(UserDetails user, String newPassword) {
        return null;
    }
}

AuthenticationManager和AuthenticationProvider,Spring Security提供了默认的实现ProviderManager和DaoAuthenticationProvider。直接在配置类配置这两个bean即可。

鉴权:
鉴权流程主要由AccessDecisionManager(鉴权管理器)和AccessDecisionVoter(投票器)来处理。鉴权管理器使用默认实现之一的UnanimousBased(一票反对,只要有一票反对就不能通过),然后实现自定义的投票器即可。
在实际鉴权处理前,我们还需要一个过滤器来处理jwt,通过jwt来拿到认证信息。
jwt过滤器:

@Slf4j
//@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Resource
    private RedisTemplate<String,String> stringRedisTemplate;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //1、拿到token
        String token = request.getHeader("token");
        if(StringUtils.isNotEmpty(token)){
            //2、校验token
            try {
                String username = JwtUtils.getUserName(token);
                String key = GlobalConstants.LOGIN_CACHE_KEY_PREFIX+username;
                String userInfoStr = stringRedisTemplate.opsForValue().get(key);
                if(StringUtils.isNotEmpty(userInfoStr)){
                    //得到用户账号及权限相关信息
                    LoginUserInfoDto loginUserInfoDto = JSONObject.parseObject(userInfoStr,LoginUserInfoDto.class);
                    //设置该用户的权限上下文信息,方便后续过滤器校验
                    UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
                            new UsernamePasswordAuthenticationToken(loginUserInfoDto,null,loginUserInfoDto.getAuthorities());
                    SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);

                }else{
                    throw new RuntimeException("token无效或已过期,请重新登录!");
                }
                //放行
                filterChain.doFilter(request,response);
            } catch (RuntimeException e) {
            	//自行处理认证异常,如果不处理的话,会由Spring Security处理,如果没定义异常处理handler,最后会返回403
                exceptionHandle(request,response,e);
            }
        }else{
            //放行
            filterChain.doFilter(request,response);
        }
    }

    /**
     * jwt认证失败处理
     * @param request
     * @param response
     * @param e
     */
    private void exceptionHandle(HttpServletRequest request, HttpServletResponse response,Exception e) throws IOException {
        log.info("jwt认证失败,原因:{}",e.getMessage());
        //这里就不往下走了,直接返回失败的结果
        Map<String,Object> result = new HashMap();
        result.put("code",-3);
        result.put("message","token认证失败!");

        //  将结果对象转换成json字符串
        String json = JSON.toJSONString(result);
        response.setContentType("application/json;charset=UTF-8");
        //  响应体
        response.getWriter().println(json);
    }
}

自定义AccessDecisionVoter(投票器):

@Slf4j
public class AccessDecisionProcessor implements AccessDecisionVoter<FilterInvocation> {

    @Resource
    private RedisTemplate<String,String> stringRedisTemplate;
    @Resource
    private PermissionMapper permissionMapper;

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }

    @Override
    public int vote(Authentication authentication, FilterInvocation object, Collection<ConfigAttribute> attributes) {
        //默认否决票
        int result = ACCESS_DENIED;
        String requestUrl = object.getRequest().getServletPath();
        String method = object.getRequest().getMethod();
        log.debug("进入自定义鉴权投票器,URI : {} {}", method, requestUrl);

        //判断请求是否运行匿名访问
        boolean anonymous = stringRedisTemplate.opsForHash().hasKey(GlobalConstants.GLOBAL_PERMISSION_ANONYMOUS,requestUrl);
        if(anonymous){
            //允许匿名访问直接同意
            return ACCESS_GRANTED;
        }
        //拿到用户的角色
        Object principal = authentication.getPrincipal();

        //principal不是LoginUserInfoDto表示是匿名用户或未认证的用户,且请求url未在数据库配置权限
        if(principal instanceof LoginUserInfoDto){
            LoginUserInfoDto dto = (LoginUserInfoDto)principal;
            List<String> roles = dto.getRoles();

            String keyPrefix = GlobalConstants.GLOBAL_PERMISSION_KEY_PREFIX;
            if(!CollectionUtils.isEmpty(roles)){
                for(String roleCode : roles){
                    String key = keyPrefix+roleCode;
                    if(stringRedisTemplate.hasKey(key)){
                        String val = (String)stringRedisTemplate.opsForHash().get(key,requestUrl);
                        if(val!=null){
                            //存在投同意
                            result = ACCESS_GRANTED;
                            //结束循环
                            break;
                        }
                    }else{
                        //如果缓存没有,查库
                        List<String> urls = permissionMapper.getPermissionUrlByRole(roleCode);
                        if(!CollectionUtils.isEmpty(urls)){
                            //存缓存
                            Map<String,Object> map = new HashMap<>();
                            urls.forEach(url ->{
                                map.put(url,"1");
                            });
                            stringRedisTemplate.opsForHash().putAll(key,map);

                            if(urls.contains(requestUrl)){
                                //存在投同意
                                result = ACCESS_GRANTED;
                                //结束循环
                                break;
                            }
                        }
                    }
                }
            }
        }else{
            //匿名用户请求,且请求url未在数据库配置权限,交给WebExpressionVoter处理,这里就不做处理
            result = ACCESS_ABSTAIN;
        }

        return result;
    }
}

这个投票器的主要逻辑是,去redis查询项目启动时初始化的角色权限缓存。没有缓存,则查库。拿到用户认证信息(在jwt过滤器里设置的)里的角色,判断角色权限缓存里有没有请求的url,有则表示该角色能访问该url,即用户有权访问该url。

初始化角色权限缓存:

@Component
@Slf4j
public class PermissionInitRunner implements ApplicationRunner {
    @Resource
    private RedisTemplate<String,String> stringRedisTemplate;
    @Resource
    private RoleMapper roleMapper;
    @Resource
    private PermissionMapper permissionMapper;
    @Override
    public void run(ApplicationArguments args) throws Exception {
        String keyPrefix = GlobalConstants.GLOBAL_PERMISSION_KEY_PREFIX;
        log.info("开始初始化全局资源权限缓存");

        List<String> allRoleCode = roleMapper.getAllRoleCode();
        if(!CollectionUtils.isEmpty(allRoleCode)){
            for(String roleCode : allRoleCode){
                List<String> urls = permissionMapper.getPermissionUrlByRole(roleCode);
                if(!CollectionUtils.isEmpty(urls)){
                    Map<String,Object> map = new HashMap<>();
                    urls.forEach(url ->{
                        map.put(url,"1");
                    });
                    stringRedisTemplate.opsForHash().putAll(keyPrefix+roleCode,map);
                }
            }
        }

        //允许匿名访问的资源权限key
        List<String> urls = permissionMapper.getAnonymousPermissionUrl();
        if(!CollectionUtils.isEmpty(urls)){
            String key = GlobalConstants.GLOBAL_PERMISSION_ANONYMOUS;
            Map<String,Object> map = new HashMap<>();
            urls.forEach(url ->{
                map.put(url,"1");
            });
            stringRedisTemplate.opsForHash().putAll(key,map);
        }
        log.info("初始化全局资源权限缓存结束");
    }
}

自定义认证异常和鉴权异常的处理类:
认证异常处理类:

public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        String localizedMessage = "未认证,请先认证!";//authException.getLocalizedMessage();

        Map<String,Object> result = new HashMap();
        result.put("code",-2);   // 告诉用户需要登录
        result.put("message",localizedMessage);   //


        //  将结果对象转换成json字符串
        String json = JSON.toJSONString(result);
        //  返回json数据到前端
        //  响应头
        response.setContentType("application/json;charset=UTF-8");
        //  响应体
        response.getWriter().println(json);
        //返回登录界面
        //response.sendRedirect(request.getContextPath()+"/myLoginPage");
    }
}

鉴权异常处理类:

public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        Map<String,Object> result = new HashMap();
        result.put("code",-1);   // 没有权限
        result.put("message","没有权限");   //


        //  将结果对象转换成json字符串
        String json = JSON.toJSONString(result);

        //  返回json数据到前端
        //  响应头
        response.setContentType("application/json;charset=UTF-8");
        //  响应体
        response.getWriter().println(json);
        //返回页面
        //response.sendRedirect(request.getContextPath()+"/main");
    }
}

Spring Security配置类:

@Configuration
public class WebSecurityConfig {

    /**
     * 密码编码器,会对请求传入的密码进行加密
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public UserDetailsService userDetailsService(){
        return new DBUserDetailsManager();
    }

    @Bean
    public AuthenticationProvider authenticationProvider(UserDetailsService  userDetailsService,
                                                         PasswordEncoder passwordEncoder){
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setUserDetailsService(userDetailsService);
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder);
        return daoAuthenticationProvider;
    }

    /**
     * 认证管理器
     * @param authenticationProvider
     * @return
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationProvider authenticationProvider){
        // ProviderManager 是 AuthenticationManager 最常用的实现
        return new ProviderManager(authenticationProvider);
    }


    /**
     * jwt过滤器
     * @return
     */
    @Bean
    public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter(){
        return new JwtAuthenticationTokenFilter();
    }

    /**
     * 自定义鉴权投票器
     * @return
     */
    @Bean
    public AccessDecisionVoter<FilterInvocation> accessDecisionProcessor() {
        return new AccessDecisionProcessor();
    }

    /**
     * 鉴权管理器
     * @return
     */
    @Bean
    public AccessDecisionManager accessDecisionManager() {
        // 构造一个新的AccessDecisionManager 放入两个投票器
        //WebExpressionVoter为配置文件投票器,即在HttpSecurity 的authorizeRequests方法里定义的过滤规则,使用他是为了也可以使用配置定义好放行规则
        List<AccessDecisionVoter<?>> decisionVoters = Arrays.asList(new WebExpressionVoter(), accessDecisionProcessor());
        //UnanimousBased为一票否决鉴权
        //AffirmativeBased为一票通过鉴权,WebExpressionVoter投票如果未配置则默认为通过,所以这里需要配置为UnanimousBased
        return new UnanimousBased(decisionVoters);
    }
    /**
     * Spring Security配置
     * @param http
     * @return
     * @throws Exception
     */
    @Bean
    public SecurityFilterChain mySecurityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests(authorize ->authorize
                // 放行所有OPTIONS请求,跨域请求会先发一个OPTIONS请求
                .antMatchers(HttpMethod.OPTIONS).permitAll()
                .antMatchers("/login").permitAll()
                .antMatchers("/myLogout").permitAll()
                .anyRequest()  //对所有请求开启授权保护
                .authenticated() //已认证的请求会被自动授权
                .accessDecisionManager(accessDecisionManager())
        );
        //添加自定义过滤器
        http.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
        http.exceptionHandling(exception -> exception
                .authenticationEntryPoint(new MyAuthenticationEntryPoint()) //请求未认证的处理
                .accessDeniedHandler(new MyAccessDeniedHandler())   //未授权资源请求处理
        );
        //关闭csrf防护,否则所有的POST的请求都需要携带CSRF令牌
        http.csrf(csrf -> csrf.disable());
        // 关闭Session机制
        //http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        return http.build();
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1961086.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

推荐5款好用的将pdf翻译成中文的工具。

像word&#xff0c;PPT,Excel等这些文档如果要翻译的话&#xff0c;即使没有合适的工具也可以复制粘贴内容。可PDF有的时候是不可以编辑的&#xff0c;很难用这种方法实现翻译。但是这5款翻译工具就可以做到直接将PDF文件进行翻译。 1、365pdf在线翻译 直达&#xff1a;https:…

力扣Hot100-543二叉树的直径

给你一棵二叉树的根节点&#xff0c;返回该树的 直径 。 二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root 。 两节点之间路径的 长度 由它们之间边数表示。 示例 1&#xff1a; 输入&#xff1a;root [1,2,3,4,5] 输出&a…

零基础入门转录组数据分析——机器学习算法之lasso(筛选特征基因)

零基础入门转录组数据分析——机器学习算法之lasso&#xff08;筛选特征基因&#xff09; 目录 零基础入门转录组数据分析——机器学习算法之lasso&#xff08;筛选特征基因&#xff09;1. Lasso基础知识2. Lasso&#xff08;Rstudio&#xff09;——代码实操2. 1 数据处理2. 2…

结构体的引入

结构体也是一种数据组合&#xff0c;它和数组的区别是&#xff0c;数组的元素类型是一样的数据集合体&#xff0c;如果元素类型不一样&#xff0c;就要用到结构体了 下面定义一个学生结构体 struct Student {int num;char name[32];int age;double score;char addr[32]; }; …

谷粒商城实战笔记-88~91-商品发布保存

文章目录 一&#xff0c;基本信息二&#xff0c;规格参数三&#xff0c;销售属性四&#xff0c;SKU信息五&#xff0c;代码分析1&#xff0c;Spu信息的保存2&#xff0c;Sku信息的保存 这一篇包含三节内容&#xff1a; 88-商品服务-API-新增商品-保存SPU基本信息89-商品服务-AP…

Redis缓存穿透、击穿和雪崩的理解和解决思路

Redis的缓存穿透 缓存穿透是指那些查询请求所要获取的数据既不在缓存&#xff08;Redis&#xff09;中&#xff0c;也不在数据库&#xff08;例如&#xff1a;MySQL&#xff09;中&#xff0c;因此每次请求都会直接访问数据库。这种情况通常由以下几种情形引起&#xff1a; 恶…

C++:类进阶之继承与派生

一、基本概念&#xff1a;继承、基类、派生类 继承&#xff1a;在定义一个新的类B时&#xff0c;如果该类与某个已有的类A相似 (指的是B拥有A的全部特点)&#xff0c;那么就可以把A作为一个基类&#xff0c;而把B作为基类的一个派生类 (也称子类)。 派生类&#xff1a;通过对…

微前端技术预研 - bit初体验

1.关于什么是微前端以及微前端的发展&#xff0c; 当前主流框架以及实现技术等&#xff0c;可参考这篇总结(非常全面)&#xff0c; 微前端总结&#xff1a;目录详见下图 本文内容主要针对bit框架的实时思路以及具体使用。 1.什么是Bit? &#xfeff;Bit 是可组合软件的构建…

《C语言实现各种排序算法》

文章目录 一、排序1、排序的各种方式分类 二、插入排序1、直接插入排序2、希尔排序3、希尔排序时间复杂度分析 三、选择排序1、直接选择排序2、堆排序 四、交换排序1、冒泡排序2、快速排序3、快速排序hoare找基准值4、快排挖坑法找基准值5、前后指针法6、快速排序非递归实现 五…

甄选范文“论数据分片技术及其应用”软考高级论文,系统架构设计师论文

论文真题 数据分片就是按照一定的规则,将数据集划分成相互独立、正交的数据子集,然后将数据子集分布到不同的节点上。通过设计合理的数据分片规则,可将系统中的数据分布在不同的物理数据库中,达到提升应用系统数据处理速度的目的。 请围绕“论数据分片技术及其应用”论题…

OCC BRepOffsetAPI_ThruSections使用

目录 一、BRepOffsetAPI_ThruSections简介 二、功能与特点 三、应用场景 四、示例 一、BRepOffsetAPI_ThruSections简介 在Open CASCADE Technology (OCCT) 中,BRepOffsetAPI_ThruSections 类是用来通过放样生成一个实体或者一个面壳(Shell)。当使用这个类时,isSolid 参…

具身智能,存内计算芯片应用新赛道

引言&#xff1a; 具身智能&#xff08;Emboided Al&#xff09;是指通过身体与环境的动态互动&#xff0c;实现对世界的感知、认知和行为控制的智能系统。具身智能强调的是智能体与环境的交互/学习/改变&#xff0c;而不仅仅是身体本身。具身智能的核心要素体现在智能体通过…

MySQL --- 数据类型

一、类型分类 数值类型bit(M)位类型&#xff0c;M指定位数&#xff0c;默认值1&#xff0c;范围1 - 64bool使用0和1表示真假tinyint [unsigned]带符号范围 -128~127&#xff0c;无符号范围 0~255&#xff0c;默认有符号smallint [unsigned]带符号范围 -2^15~2^15-1&#xff0c…

【网络世界】HTTPS协议

目录 &#x1f308;前言&#x1f308; &#x1f4c1; HTTP缺陷 &#x1f4c1; HTTPS &#x1f4c2; 概念 &#x1f4c2; 加密 &#x1f4c2; 加密方式 &#x1f4c1; 中间人攻击 &#x1f4c1; CA机构和证书 &#x1f4c2; 数据摘要&#xff08;数据指纹&#xff09; &…

nginx反向代理和负载均衡+安装jdk-22.0.2

ps -aux|grep nginx //查看进程 nginx 代理 nginx代理是负载均衡的基础 主机&#xff1a;192.168.118.60 这台主机只发布了web服务&#xff0c;没有做代理的任何操作 修改一下index.html中的内容 echo "this is java web server" > /usr/local/nginx/htm…

【OpenCV-Python实战项目】26-实时手部跟踪

0 介绍 目的&#xff1a;使用mediapipe库做手部的实时跟踪 检测流程&#xff1a;&#xff08;1&#xff09;手掌检测&#xff1b;&#xff08;2&#xff09;手掌特征检测 手掌特征分布&#xff1a;mediapipe手掌特征分布如下&#xff1a; 1.环境要求 后续代码运行环境&…

力扣SQL50 换座位

Problem: 626. 换座位 &#x1f468;‍&#x1f3eb; 参考题解 Code SELECT(CASEWHEN MOD(id, 2) ! 0 AND counts ! id THEN id 1WHEN MOD(id, 2) ! 0 AND counts id THEN idELSE id - 1END) AS id,student FROMseat,(SELECTCOUNT(*) AS countsFROMseat) AS seat_counts O…

电测量数据交换DLMSCOSEM组件第53部分:DLMSCOSEM应用层(中)

2.综述 (续上篇) 上篇地址:http://t.csdnimg.cn/DBKrg 2.2DLMS/COSEM应用层主要特点 2.2.1DLMS/COSEM应用层结构 DLMS/COSEM AL的主要部件是应用服务对象(ASO)。它给其服务用户提供服务(COSEM应用进程),并使用支撑层提供的服务。客户机和服务器侧都包含三个必…

Image Caption评估指标深入理解

前言&#xff1a;刚开始做图像描述的任务就整理了这些评估指标&#xff0c;时间久远有点记不清怎么具体实现原理了&#xff0c;结果面试的时候就问了这个问题&#xff0c;没答上来&#xff0c;郁闷了很久&#xff0c;还是基础不扎实&#xff0c;浅浅记录一下 文章目录 BLEUROUG…

C语言的结构体

结构体定义 结构体指针