SpringBoot整合Shiro+Jwt

springboot整合shiro+jwt

学习博客链接:https://blog.csdn.net/wws_p/article/details/107126321(万分感谢)

一、搭建数据库环境

CREATE
DATABASE `shiro`;

USE
`shiro`;

DROP TABLE IF EXISTS `role_per`;

CREATE TABLE `role_per`
(
    `id`     int(11) NOT NULL AUTO_INCREMENT COMMENT '表主键id',
    `roleId` varchar(32) DEFAULT NULL COMMENT '角色表的主键id',
    `perId`  varchar(32) DEFAULT NULL COMMENT '权限表的主键id',
    PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

insert into `role_per`(`id`, `roleId`, `perId`)
values (1, '100', 'M01'),
       (2, '100', 'M02'),
       (3, '100', 'M03'),
       (4, '200', 'M204'),
       (5, '100', 'M204');

DROP TABLE IF EXISTS `sys_permissions`;

CREATE TABLE `sys_permissions`
(
    `perId`           varchar(32) NOT NULL COMMENT '权限表id 作为表主键 用于关联',
    `permissionsName` varchar(32)  DEFAULT NULL COMMENT '权限名称',
    `perRemarks`      varchar(255) DEFAULT NULL COMMENT '备注,预留字段',
    PRIMARY KEY (`perId`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;


insert into `sys_permissions`(`perId`, `permissionsName`, `perRemarks`)
values ('M01', 'resetPassword', '重置密码'),
       ('M02', 'querySystemLog', '查看系统日志'),
       ('M03', 'exportUserInfo', '导出用户信息'),
       ('M204', 'queryMyUserInfo', '查看个人信息');

DROP TABLE IF EXISTS `sys_role`;

CREATE TABLE `sys_role`
(
    `roleId`      varchar(32) NOT NULL COMMENT ' 角色id 作为表主键 用于关联',
    `roleName`    varchar(32)  DEFAULT NULL COMMENT '角色名',
    `roleRemarks` varchar(255) DEFAULT NULL COMMENT '备注,预留字段',
    PRIMARY KEY (`roleId`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

insert into `sys_role`(`roleId`, `roleName`, `roleRemarks`)
values ('100', 'admin', '系统管理员'),
       ('200', 'common', '普通用户');


DROP TABLE IF EXISTS `sys_user`;

CREATE TABLE `sys_user`
(
    `id`          int(11) NOT NULL AUTO_INCREMENT COMMENT 'user表的id字段',
    `userId`      varchar(255) NOT NULL COMMENT '用户id 作为表主键 用于关联',
    `userName`    varchar(25)  DEFAULT NULL COMMENT '用户登录帐号',
    `password`    varchar(255) DEFAULT NULL COMMENT '用户登录密码',
    `userRemarks` varchar(255) DEFAULT NULL COMMENT '备注,预留字段',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

insert into `sys_user`(`id`, `userId`, `userName`, `password`, `userRemarks`)
values (1, '10001', 'adminHong', '202cb962ac59075b964b07152d234b70', '小红'),
       (2, '15564819717', '2', 'f10bfea990b657f6e8355c6e3ee67e2d', 'hehe'),
       (3, '177900', '武文生', 'c4dbde004d409a733e4a7c8b00466613', '生生酱'),
       (4, '20001', 'jc', 'e165421110ba03099a1c0393373c5b43', 'JC'),
       (5, '18864819717', 'myDear', '756b3c2e758a2b8c728fa2e4d3f3294d', 'nice today i love you'),
       (8, '18262699169', 'hello', '4cad6da13952ad1621e4f8ede54d9fad', 'hello'),
       (9, '15698756214', 'good luck', '713741121ec6b5d854b9c15e78a36f27', 'good luck'),
       (10, '12345678922', '测试一下', 'a753bbccb874ef05b43b9fceffb949dd', '闲来无聊');


DROP TABLE IF EXISTS `user_role`;

CREATE TABLE `user_role`
(
    `id`     int(11) NOT NULL AUTO_INCREMENT COMMENT '表主键id',
    `userId` varchar(255) DEFAULT NULL COMMENT '帐号表的主键id',
    `roleId` varchar(32)  DEFAULT NULL COMMENT '角色表的主键id',
    PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

insert into `user_role`(`id`, `userId`, `roleId`)
values (1, '10001', '100'),
       (2, '20001', '200'),
       (3, '177900', '200'),
       (4, '15564819717', '200'),
       (5, '18864819717', '200'),
       (8, '18262699169', '200'),
       (9, '15698756214', '200'),
       (10, '12345678922', '200');

二、搭建项目环境

  1. 创建spring-boot项目
  2. 导入依赖

<dependencies>
    
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-webartifactId>
    dependency>
    
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-testartifactId>
        <scope>testscope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintagegroupId>
                <artifactId>junit-vintage-engineartifactId>
            exclusion>
        exclusions>
    dependency>
    
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-data-redisartifactId>
    dependency>
    
    <dependency>
        <groupId>mysqlgroupId>
        <artifactId>mysql-connector-javaartifactId>
        <scope>runtimescope>
    dependency>
    
    <dependency>
        <groupId>org.projectlombokgroupId>
        <artifactId>lombokartifactId>
        <version>1.18.10version>
    dependency>
    
    <dependency>
        <groupId>org.mybatis.spring.bootgroupId>
        <artifactId>mybatis-spring-boot-starterartifactId>
        <version>2.0.1version>
    dependency>
    
    <dependency>
        <groupId>org.apache.shirogroupId>
        <artifactId>shiro-springartifactId>
        <version>1.3.2version>
    dependency>
    <dependency>
        <groupId>javax.persistencegroupId>
        <artifactId>persistence-apiartifactId>
        <version>1.0version>
    dependency>
    
    <dependency>
        <groupId>com.auth0groupId>
        <artifactId>java-jwtartifactId>
        <version>3.4.0version>
    dependency>
    <dependency>
        <groupId>com.google.guavagroupId>
        <artifactId>guavaartifactId>
        <version>28.1-jreversion>
    dependency>
    <dependency>
        <groupId>junitgroupId>
        <artifactId>junitartifactId>
        <scope>testscope>
    dependency>
dependencies>
  1. 为了保证java包下的xml文件成功导出
# pom.xml中添加配置
<build>
    <resources>
        <resource>
            <directory>src/main/javadirectory>
            <includes>
                <include>**/*.xmlinclude>
            includes>
        resource>
    resources>
build>
  1. 书写配置文件
# 端口
server.port=8081
# 数据库配置
spring.datasource.url=jdbc:mysql://localhost:3306/shiro?useUnicode=true&characterEncoding=utf8&useSSL=true&serverTimezone=GMT%2B8
spring.datasource.password=123456
spring.datasource.username=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# mybatis配置
mybatis.mapper-locations=classpath:com.example.mapper/*.xml
mybatis.type-aliases-package=com.example.pojo
# 开启驼峰命名
mybatis.configuration.map-underscore-to-camel-case=true
  1. 主启动类上添加@MapperScan注解

@SpringBootApplication
@MapperScan("com.example.mapper")
public class ShiroJwtApplication {
    public static void main(String[] args) {
        SpringApplication.run(ShiroJwtApplication.class, args);
    }
}
  1. 创建User类(实现表-实体对应)

@Getter
@ToString
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(callSuper = false, of = "userId")
@Builder
@Table(name = "sys_user")
@Accessors(chain = true)
public class User {
    private Integer id;
    private String userId;
    private String userName;
    private String password;
    private String userRemarks;


    public static User getUser(String userId, String userName, String encryptPassword, String remark) {
        return User.builder().userId(userId).userName(userName).password(encryptPassword).userRemarks(remark).build();
    }
}
  1. 书写操作数据库代码,并进行测试

Mapper接口


@Mapper
public interface UserMapper {
    User selectById(@Param("userId") String userId);

    LinkedHashMap<String, Object> selectUserPermissionById(@Param("userId") String userId);

    User selectByName(@Param("userName") String userName);

    LinkedHashMap<String, Object> selectUserPermissionByName(@Param("userName") String userId);

    void insertUser(@Param("user") User user);
}

对应的xml文件




    
    
    
    
    
    
    
    
    
        INSERT into sys_user
        (userId, userName, password, userRemarks)
        VALUES (#{user.userId}, #{user.userName}, #{user.password}, #{user.userRemarks})
    

测试类进行测试


@RunWith(SpringRunner.class)
@SpringBootTest(classes = ShiroJwtApplication.class)
public class ShiroApplicationTest {
    @Autowired
    private UserMapper userMapper;

    @Test
    public void test01() {
        LinkedHashMap<String, Object> map = userMapper.selectUserPermissionById("10001");
        System.out.println(map);
    }
}

三、用户注册模块实现

  1. 书写用户注册dto

@Getter
@Setter
@ToString
public class RegisterDto {
    //用户手机号
    @NotNull(message = "用户输入的手机号不可为空")
    @Size(max = 11, min = 11, message = "手机号必须为11位")
    private String userId;

    //用户输入的密码
    @NotBlank(message = "用户输入的注册密码不可为空")
    @Size(max = 8, min = 6, message = "用户输入的密码长度必须在6-8之间")
    private String password;

    //用户名称
    private String userName;

    //描述
    private String remark;
}
  1. 书写用户注册接口
@PostMapping("/user/register")
public Map<String, Object> userRegister(@RequestBody @Valid RegisterDto registerDto){
        return userService.register(registerDto.getUserId(),registerDto.getUserName(),registerDto.getPassword(),registerDto.getRemark());
        }
  1. 书写服务层注册接口

@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserMapper userMapper;

    @Autowired
    private UserRoleMapper userRoleMapper;


    @Override
    public Map<String, Object> passWordLogin(String userId, String passWord) {
        HashMap<String, Object> map = new HashMap<>();
        // 首先检查此用户是否在数据库
        if (this.userMapper.selectById(userId) != null) {
            map.put("code", "500");
            map.put("msg", "用户id已存在");
            return map;
        }
        // 制作用户密码,然后将用户插入user表中
        // SimpleHash(加密的算法名称,密码,随机盐,散列次数)
        String newPassword = new SimpleHash("md5", password, userId, 1024).toHex();
        log.info("加密之后的用户密码是:{}", newPassword);

        this.insetUser(User.getUser(userId, userName, newPassword, remark));
        // 增加用户角色中间表,注册最基本角色
        userRoleMapper.insert(userId, 200);
        map.put("code", 200);
        map.put("msg", "注册成功");
        return map;
    }

    @Override
    public HashMap<String, Object> register(String userId, String userName, String password, String remark) {
        return userDomainService.register(userId, userName, password, remark);
    }

    @Override
    public void sendVerificationCode(String userId) {

    }

    @Override
    public String verificationCodeLogin(String userId, String code) {
        return null;
    }
}
  1. 使用postman进行测试

四、用户登录实现

整合思路:登录的时候要求用户输入手机号+密码登录,登录之后首先检查有无此用户,没有提示用户不存在,有的话继续检查用户输入的密码是否正确,正确为用户返回jwt制作的token,错误进行友情提示。

  1. 书写用户登录Dto
@Getter
@Setter
@ToString
public class PassWordLoginDto {
    @NotNull(message = "用户密码登录传递的id不能为空")
    private String userId;
    @NotBlank(message = "用户密码登录传递的密码名称不能为空")
    private String password;
}
  1. 用户登录(controller层)接口
@PostMapping(value = "/user/passwordLogin", name = "用户密码登录")
 public Map<String, Object> passwordLogin(@RequestBody @Valid PassWordLoginDto passWordLoginDTO) {
   log.info("传递的请求参数:{}", passWordLoginDTO);
  return userService.passWordLogin(passWordLoginDTO.getUserId(),passWordLoginDTO.getPassword());
 }
  1. 用户登录(Service层)接口(具体处理方法)
 public Map<String, Object> passWordLogin(String userId, String passWord) {
        HashMap<String, Object> map = new HashMap<>();
        // 获取Subject
        Subject subject = SecurityUtils.getSubject();
        // 校验userId是否为空
        if (StringUtils.isEmpty(userId)) {
            map.put("code", 500);
            map.put("msg", "账户不存在");
            return map;
        }
        // 校验数据库中此user是否存在
        User user = this.userMapper.selectById(userId);
        if (user == null) {
            map.put("code", 500);
            map.put("msg", "用户不存在");
            return map;
        }
        // 制作CustomizedToken执行登录
        CustomizedToken customizedToken = new CustomizedToken(userId, passWord, LoginEnum.BY_PASSWORD.getLoginType());
        subject.login(customizedToken);
        // 若登陆成功,生成token返回给前端
        String sign = JwtUtil.sign(user.getUserId(), Constant.TOKEN_SECRET);
        map.put("code", 200);
        // 返回Token信息
        map.put("token", sign);
        return map;
    }

五、相关使用类配置

  1. CustomizedTokens实体类(其中loginType可以方便我们来扩展登录方式)
public class CustomizedToken extends UsernamePasswordToken {

    /**
     * 登录类型
     */
    public String loginType;

    public CustomizedToken(final String username, final String password, final String loginType) {
        super(username, password);
        this.loginType = loginType;
    }

    public String getLoginType() {
        return loginType;
    }

    public void setLoginType(String loginType) {
        this.loginType = loginType;
    }


    @Override
    public String toString() {
        return "loginType=" + loginType + ",username=" + super.getUsername() + ",password=" + String.valueOf(super.getPassword());
    }
}
  1. 常量类
public class Constant {

    /**
     * 验证码过期时间 此处为五分钟
     */
    public static final Integer CODE_EXPIRE_TIME = 60 * 5;

    /**
     * jwtToken过期时间 默认为30天
     * public static Integer TOKEN_EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000;
     */
    public static final Long TOKEN_EXPIRE_TIME = 31 * 24 * 60 * 60 * 1000L;

    /**
     * UserId
     */
    public static final String USER_ID = "userId";


    /**
     * token请求头名称
     */
    public static final String TOKEN_HEADER_NAME = "authorization";

    /*做token签名的字符串*/
    public static final String TOKEN_SECRET="abc";

    //token的载荷中盛放的信息 只盛放一个userId 其余什么也不再盛放
    public static final String TOKEN_CLAIM="userId";

    //redis存放用户验证码时给的前缀
    public static final String REDIS_LOGIN_CODE="LOGIN_CODE:";
}
  1. JWTUtils(Token生成工具类)
@Slf4j
public class JwtUtil {
    /**
     * 校验token是否正确
     *
     * @param token  密钥
     * @param secret 用户的密码
     * @return 是否正确
     */
    public static boolean verify(String token, String secret) {
        try {
            //根据密码生成JWT效验器
            Algorithm algorithm = Algorithm.HMAC256(secret);
            JWTVerifier verifier = JWT.require(algorithm)
                    .withClaim(Constant.TOKEN_CLAIM, getUserId(token))
                    .build();
            // 效验TOKEN
            verifier.verify(token);
            return true;
        } catch (JWTVerificationException exception) {
            return false;
        }
    }


    /**
     * 获得token中的信息无需secret解密也能获得
     *
     * @return token中包含的用户名
     */
    public static String getUserId(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim(Constant.TOKEN_CLAIM).asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    /**
     * @param userId 用户id
     * @param secret 制作此token的签名依据(使用Constant.TOKEN_SECRET)
     * @return 加密的token
     */
    public static String sign(String userId, String secret) {
        Date date = new Date(System.currentTimeMillis() + Constant.TOKEN_EXPIRE_TIME);
        Algorithm algorithm = Algorithm.HMAC256(secret);
        return JWT.create()
                .withClaim(Constant.TOKEN_CLAIM, userId)
                .withExpiresAt(date)
                .sign(algorithm);

    }
}

六、Shiro配置

1. 将书写的Realm类交给Shiro管理
2. 进行加密规则统一处理
3. 进行ShiroFilter编写,实现请求拦截和进行多Realm处理
realm是shiro的核心概念,不同的realm用于不同情形下用户的身份认证和权限校验,写一个realm 需要继承shiro的AuthorizingRealm 。你需要完成的需重写两个方法:doGetAuthenticationInfodoGetAuthorizationInfo。分别负责刚刚提到的身份认证和权限校验。当存在多种认证的token的时候,你需要重写这里的supports方法,起到的是一个适配的作用。
原文链接:https://blog.csdn.net/wws_p/article/details/107126321
  1. 配置PasswordRealm(主要用来处理CustomizedToken中loginType为密码的登录方式)
/**
 * 不需要关注PasswordRealm的授权
 * 用户登录之后的权限校验全部发生在token层面,由jwtRealm进行
 * PasswordRealm要做的就是用户的认证
 */
@Slf4j
public class PasswordRealm extends AuthorizingRealm {

    //认为realm所做的身份认证、权限校验都属于最底部的进入接口前的准备 尽量让其贴近领域层
    @Autowired
    private UserMapper userMapper;

    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof CustomizedToken;
    }

    /**
     * 授权操作
     *
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    /**
     * 认证操作
     *
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        log.info("PasswordRealm权限认证开始,传递的token:{}", authenticationToken);
        //找出数据库中的对象  给定用户输入的对象做出对比
        CustomizedToken token = (CustomizedToken) authenticationToken;
        log.info("PasswordRealm转换的自定义token是:{}", token);
        // 根据userId查询用户
        User user = userMapper.selectById(token.getUsername());
        if (user == null) {
            // 抛出账号不存在异常
            throw new UnknownAccountException();
        }
        Object credentials = user.getPassword();
        //param1:数据库用户 param2:密码 param3:加密所用盐值 param4:当前realm的名称
        return new SimpleAuthenticationInfo(user, credentials, ByteSource.Util.bytes(user.getUserId()), getName());
    }
}
  1. 配置JwtRealm(处理后续使用JwtToken的携带方式)
@Slf4j
public class JwtRealm extends AuthorizingRealm {

    @Autowired
    private UserMapper userMapper;

    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        String token = principalCollection.toString();
        String userId = JwtUtil.getUserId(token);
        log.info("JwtRealm身份认证开始,获取到的token是:{}", token);
        // 这里的空指针异常不需要处理 无论此处抛出什么异常 shiro均认为身份有问题
        LinkedHashMap<String, Object> stringObjectLinkedHashMap = userMapper.selectUserPermissionById(userId);
        //获取所有的角色
        String roleName = String.valueOf(stringObjectLinkedHashMap.get("roleName"));
        //获取所有的权限
        List<String> permissionsNameList =
                Splitter.on(",").splitToList(String.valueOf(stringObjectLinkedHashMap.get("permissionsNameList")));
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        // 添加角色
        authorizationInfo.addRole(roleName);
        //添加权限
        authorizationInfo.addStringPermissions(permissionsNameList);
        return authorizationInfo;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        log.info("jwtRealm中关于身份验证的方法执行...传递的token是:{}", authenticationToken);
        String token = (String) authenticationToken.getCredentials();
        // 解密获得phone,用于和数据库进行对比
        String userId = JwtUtil.getUserId(token);
        if (userId == null) {
            throw new AuthenticationException("账户不存在");
        }
        User user = userMapper.selectById(userId);
        //校验用户是否存在
        if (user == null) {
            throw new AuthenticationException("用户不存在");
        }
        //操作时校验的是非对称加密是否成立.
        if (!JwtUtil.verify(token, Constant.TOKEN_SECRET)) {
            log.info("token校验无效...");
            throw new AuthenticationException("token已失效");
        }

        log.info("进行身份验证时,用户提供的token有效");
        return new SimpleAuthenticationInfo(token, token, getName());
    }
}
  1. JwtToken书写
public class JwtToken implements AuthenticationToken {
    /**
     * 秘钥
     */
    private String token;

    @Override
    public Object getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return token;
    }

    public JwtToken(String token) {
        this.token = token;
    }
}
  1. 如今拥有多个Realm对象,如何选择使用那个Realm对象来进行处理(书写UserModularRealmAuthenticator来继承ModularRealmAuthenticator来实现Realm的选择)
@Slf4j
public class UserModularRealmAuthenticator extends ModularRealmAuthenticator {

    //当subject.login()方法执行,下面的代码即将执行
    @Override
    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
        log.info("UserModularRealmAuthenticator:method doAuthenticate() 执行 ");
        // 判断getRealms()是否返回为空
        assertRealmsConfigured();

        // 所有Realm
        Collection<Realm> realms = getRealms();
        // 盛放登录类型对应的所有Realm集合
        Collection<Realm> typeRealms = new ArrayList<>();

        // 强制转换回自定义的Token
        try {
            log.info("进入了UserModularRealmAuthenticator类...得到的authenticationToken是:{}", authenticationToken);
            JwtToken jwtToken = (JwtToken) authenticationToken;
            for (Realm realm : realms) {
                if (realm.getName().contains("Jwt")) {
                    typeRealms.add(realm);
                }
            }
            return doSingleRealmAuthentication(typeRealms.iterator().next(), jwtToken);
        } catch (ClassCastException e) {
            typeRealms.clear();
            // 这个类型转换的警告不需要再关注 如果token错误 后面将会抛出异常信息
            CustomizedToken customizedToken = (CustomizedToken) authenticationToken;
            // 登录类型
            String loginType = customizedToken.getLoginType();
            for (Realm realm : realms) {
                System.out.println("real名称:" + realm.getName());
                log.info("正在遍历的realm是:{}", realm.getName());
                if (realm.getName().contains(loginType)) {
                    log.info("当前realm:{}被注入:", realm.getName());
                    typeRealms.add(realm);
                }
            }
            // 判断是单Realm还是多Realm
            if (typeRealms.size() == 1) {
                log.info("一个realm");
                return doSingleRealmAuthentication(typeRealms.iterator().next(), customizedToken);
            } else {
                log.info("多个realm");
                return doMultiRealmAuthentication(typeRealms, customizedToken);
            }
        }
    }
}
当subject.login()执行,传递的token将来到重写了ModularRealmAuthenticator类的类里面,在这里也就是来到了 UserModularRealmAuthenticator ,选取realm的规则代码中写的非常清楚。简单再说一下
首先尝试将token强转为jwtToken,因为不知道此时是用户在密码登录、验证码登录或者是jwt登录。所以只能尝试转换了。转换成功选择JwtRealm。强转失败,就只剩下两个选择,要么密码登录要么验证码登录,这两种登录传递的token都是CustomizedToken,此时根据CustomizedToken中的loginType字段选取realm即可。

原文链接:https://blog.csdn.net/wws_p/article/details/107126321
  1. 书写JwtFilter拦截所有使用JwtToken方式的请求
/**
 * 这个类最主要的目的是:当请求需要校验权限,token是否具有权限时,构造出主体subject执行login()
 */
@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter {

    /**
     * 执行登录认证
     * @param request ServletRequest
     * @param response ServletResponse
     * @param mappedValue mappedValue
     * @return 是否成功
     */
    @Override
    //这个方法叫做  尝试进行登录的操作,如果token存在,那么进行提交登录,如果不存在说明可能是正在进行登录或者做其它的事情 直接放过即可
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        try {
            executeLogin(request, response);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
 
    /**
     * 执行登录
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws ServletException, IOException {
        log.info("进入JwtFilter类中...");
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String token = httpServletRequest.getHeader(Constant.TOKEN_HEADER_NAME);
        log.info("获取到的token是:{}",token);
        // 判断token是否存在
        if (token == null) {
            return false;
        }
        JwtToken jwtToken = new JwtToken(token);
        try{
            log.info("提交UserModularRealmAuthenticator决定由哪个realm执行操作...");
            getSubject(request, response).login(jwtToken);
        } catch (AuthenticationException e){
            log.info("捕获到身份认证异常");
                return false;
        }
        return true;
    }



    /**
     * 对跨域提供支持
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
        // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }
}
  1. 书写加密规则
@Bean("matcher")
 public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        // 设置加密算法名称
        matcher.setHashAlgorithmName("MD5");
        // 设置散列次数
        matcher.setHashIterations(1024);
        // 设置存储凭证十六进制编码
        matcher.setStoredCredentialsHexEncoded(true);
        return matcher;
  }
  1. Shiro配置(完整配置,仅对于当前项目)
/**
 * shiro配置
 */
@Configuration
public class ShiroConfig {

    /**
     * 设置密码加密规则
     *
     * @return HashedCredentialsMatcher
     */
    @Bean("matcher")
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        // 设置加密算法名称
        matcher.setHashAlgorithmName("MD5");
        // 设置散列次数
        matcher.setHashIterations(1024);
        // 设置存储凭证十六进制编码
        matcher.setStoredCredentialsHexEncoded(true);
        return matcher;
    }

    /**
     * 为登录Realm绑定密码加密规则
     *
     * @param matcher
     * @return
     */
    @Bean
    public PasswordRealm passwordRealm(@Qualifier("matcher") HashedCredentialsMatcher matcher) {
        PasswordRealm passwordRealm = new PasswordRealm();

        passwordRealm.setCredentialsMatcher(matcher);
        return passwordRealm;
    }

    @Bean("jwtRealm")
    public JwtRealm jwtRealm() {
        return new JwtRealm();
    }

    /**
     * 用来处理不同的Token
     *
     * @return
     */
    @Bean("userModularRealmAuthenticator")
    public UserModularRealmAuthenticator userModularRealmAuthenticator() {
        //自己重写的ModularRealmAuthenticator
        UserModularRealmAuthenticator modularRealmAuthenticator = new UserModularRealmAuthenticator();
        modularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
        return modularRealmAuthenticator;
    }

    /**
     * Shiro内置过滤器,可以实现拦截器相关的拦截器
     * 常用的过滤器:
     * anon:无需认证(登录)可以访问
     * authc:必须认证才可以访问
     * user:如果使用rememberMe的功能可以直接访问
     * perms:该资源必须得到资源权限才可以访问
     * role:该资源必须得到角色权限才可以访问
     **/
    @Bean
    public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        // 设置 SecurityManager
        bean.setSecurityManager(securityManager);
        // 设置未登录跳转url
        bean.setUnauthorizedUrl("/user/unLogin");

        Map<String, String> filterMap = new LinkedHashMap<>();
        filterMap.put("/user/passwordLogin", "anon");
        filterMap.put("/user/verificationCodeLogin", "anon");
        filterMap.put("/user/register", "anon");
        bean.setFilterChainDefinitionMap(filterMap);
        Map<String, Filter> filter = new HashMap<>(1);
        filter.put("jwt", new JwtFilter());
        bean.setFilters(filter);
        filterMap.put("/**", "jwt");
        bean.setFilterChainDefinitionMap(filterMap);
        return bean;
    }

    /**
     * SecurityManager 是 Shiro 架构的核心,通过它来链接Realm和用户(文档中称之为Subject.)
     *
     * @return
     */
    @Bean("securityManager")
    public DefaultWebSecurityManager securityManager(@Qualifier("passwordRealm") PasswordRealm passwordRealm,
                                                     @Qualifier("jwtRealm") JwtRealm jwtRealm,
                                                     @Qualifier("userModularRealmAuthenticator") UserModularRealmAuthenticator userModularRealmAuthenticator) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        List<Realm> realms = new ArrayList<>();
        // 添加多个realm
        securityManager.setAuthenticator(userModularRealmAuthenticator);
        realms.add(passwordRealm);
        realms.add(jwtRealm);
        securityManager.setRealms(realms);

        /*
         * 关闭shiro自带的session,详情见文档
         */
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);

        return securityManager;
    }

    /**
     * 以下Bean开启shiro权限注解
     *
     * @return DefaultAdvisorAutoProxyCreator
     */
    @Bean
    public static DefaultAdvisorAutoProxyCreator creator() {
        DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
        creator.setProxyTargetClass(true);
        return creator;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    @Bean(name = "lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }
}
  1. 使用postMan进行测试

七、权限校验

使用注解对接口进行权限限制(这里主要关注JwtRealm和JwtFilter操作)

@RestController
public class PremController {
    @RequiresRoles(value = {"admin", "common"}, logical = Logical.OR)
    @RequiresPermissions(value = {"resetPassword", "queryMyUserInfo"}, logical = Logical.OR)
    @GetMapping("/shiro/getPre")
    public String getUserPermission() {
        return "查询成功";
    }
}

八、使用到的枚举类和全局异常捕捉

枚举

@Getter
public enum LoginEnum {

    /**
     * 账号密码登录
     */
    BY_PASSWORD("Password"),

    /**
     * 账号密码登录
     */
    BY_CODE("Code");

    private String loginType;

    LoginEnum(String loginType) {
        this.loginType = loginType;
    }
}

全局异常捕捉

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler(value = IncorrectCredentialsException.class)
    public Map<String, Object> handlerException(IncorrectCredentialsException e) {
        HashMap<String, Object> map = new HashMap<>();
        map.put("code", 500);
        map.put("msg", "密码错误");
        return map;
    }
}

你可能感兴趣的:(SpringBoot,spring,boot,shiro,jwt,spring)