美文网首页
spring boot 整合shiro 、jwt

spring boot 整合shiro 、jwt

作者: 侧耳倾听y | 来源:发表于2021-07-18 22:39 被阅读0次

参考:
https://blog.csdn.net/qq_43948583/article/details/104437752
https://blog.csdn.net/weixin_42375707/article/details/111145907

计划实现增删改查级别的权限控制

1. 表结构

菜单表

CREATE TABLE `menu` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '菜单名称',
  `parent_id` bigint(20) NOT NULL COMMENT '父节点id',
  `url` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '路径',
  `sort_id` int(11) DEFAULT '0' COMMENT '排序ID',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='菜单表';

菜单-操作表

CREATE TABLE `menu_opt` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `menu_id` bigint(20) NOT NULL COMMENT '菜单ID',
  `opt_id` bigint(20) NOT NULL COMMENT '操作ID',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='菜单-操作关联表';

操作表

CREATE TABLE `opt` (
  `opt_id` bigint(20) NOT NULL AUTO_INCREMENT,
  `opt_name` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '操作名称',
  PRIMARY KEY (`opt_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='操作表';

角色表

CREATE TABLE `role` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '角色名称',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

角色-菜单-操作表

CREATE TABLE `role_menu_opt` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `role_id` bigint(20) NOT NULL COMMENT '角色ID',
  `menu_id` bigint(20) NOT NULL COMMENT '菜单ID',
  `opt_id` bigint(20) NOT NULL COMMENT '操作ID',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色-菜单-操作关联表表';

用户表

CREATE TABLE `sys_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(100) NOT NULL,
  `pwd` varchar(255) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

用户角色表

CREATE TABLE `user_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) NOT NULL COMMENT '用户id',
  `role_id` bigint(20) NOT NULL COMMENT '角色ID',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色表';

2. 使用mybatis plus自动生成一些类

可以参考:https://www.jianshu.com/p/c984ac0b67e8

3. 添加shiro、jwt依赖

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-starter</artifactId>
            <version>1.5.3</version>
        </dependency>

        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.8.3</version>
        </dependency>

4. shiro、jwt配置

  • shiro config
@Configuration
public class ShiroConfig {

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        //关联 DefaultWebSecurityManager
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);

        //添加shiro内置的过滤器
        /*
         * anon: 无需认证就可以访问
         * auth: 必须认证才能访问
         * user: 必须拥有记住我功能才能用
         * perms: 拥有对某个资源的权限才能访问
         * role: 拥有某个角色权限才能访问
         * */
        // 添加过滤器
        Map<String, Filter> filters = new HashMap<>(4);
        // 自定义的认证授权过滤器
        filters.put("auth", new AuthFilter());
        // 添加自定义的认证授权过滤器
        shiroFilterFactoryBean.setFilters(filters);

        //要拦截的路径放在map里面
        Map<String, String> filterMap = new LinkedHashMap<>();
        // 放行login接口
        filterMap.put("/login", "anon");
        // 放行logout接口
        filterMap.put("/logout", "anon");
        // 拦截所有路径, 它自动会跑到 AuthFilter这个自定义的过滤器里面
        filterMap.put("/**", "auth");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
        return shiroFilterFactoryBean;
    }

    @Bean
    public DefaultWebSecurityManager defaultWebSecurityManager(AuthRealm authRealm) {
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();

        //关联realm
        defaultWebSecurityManager.setRealm(authRealm);

        /*
         * 关闭shiro自带的session
         */
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        defaultWebSecurityManager.setSubjectDAO(subjectDAO);
        return defaultWebSecurityManager;
    }

    // 将自定义realm注入到 DefaultWebSecurityManager
    @Bean
    public AuthRealm authRealm() {
        return new AuthRealm();
    }

    // 通过调用Initializable.init()和Destroyable.destroy()方法,从而去管理shiro bean生命周期
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    // 开启shiro权限注解
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager defaultWebSecurityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(defaultWebSecurityManager);
        return advisor;
    }
}
  • realm
public class AuthRealm extends AuthorizingRealm {

    @Autowired
    private SysUserMapper sysUserMapper;

    @Autowired
    private UserRoleMapper userRoleMapper;

    @Autowired
    private RoleMenuOptMapper roleMenuOptMapper;

    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // 获取前端传来的token
        String accessToken = (String) token.getPrincipal();
        // 根据token去缓存里查找用户名
        String userId = JwtUtils.getAudience(accessToken);
        if (userId == null) {
            // 查找的用户名为空,即为token失效
            throw new IncorrectCredentialsException("token失效,请重新登录");
        }
        JwtUtils.verifyToken(accessToken, userId);
        SysUser user = sysUserMapper.selectById(Long.valueOf(userId));
        if (user == null) {
            throw new UnknownAccountException("用户不存在!");
        }
        // 此方法需要返回一个AuthenticationInfo类型的数据
        // 因此返回一个它的实现类SimpleAuthenticationInfo,将user以及获取到的token传入它可以实现自动认证

        return new SimpleAuthenticationInfo(user, accessToken, "");
    }

    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //从认证那里获取到用户对象User
        SysUser user = (SysUser) principals.getPrimaryPrincipal();
        // 此方法需要一个AuthorizationInfo类型的返回值,因此返回一个它的实现类SimpleAuthorizationInfo
        // 通过SimpleAuthorizationInfo里的addStringPermission()设置用户的权限
        QueryWrapper<UserRole> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("user_id", user.getId());
        List<UserRole> userRoles = userRoleMapper.selectList(queryWrapper);
        List<Long> roleIds = userRoles.stream().map(UserRole::getRoleId).collect(Collectors.toList());
        QueryWrapper<RoleMenuOpt> optWrapper = new QueryWrapper<>();
        optWrapper.in("role_id", roleIds);
        List<RoleMenuOpt> roleMenuOpts = roleMenuOptMapper.selectList(optWrapper);
        List<String> optList = roleMenuOpts.stream().map(opt -> String.valueOf(opt.getOptId()))
                .collect(Collectors.toList());
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.addStringPermissions(optList);

        return simpleAuthorizationInfo;
    }

//    @Override
//    public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
//        //自定义密码匹配器
//        MyCredentialsMatcher currentCredentialsMatcher = new MyCredentialsMatcher();
//        super.setCredentialsMatcher(currentCredentialsMatcher);
//    }
}
  • shiro filter
public class AuthFilter extends AuthenticatingFilter {


    // 生成自定义token
    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        //从header中获取token
        String token = httpServletRequest.getHeader("token");
        return new JwtToken(token);
    }

    // 所有请求全部拒绝访问
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        // 允许option请求通过
        return ((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name());
    }

    // 拒绝访问的请求,onAccessDenied方法先获取 token,再调用executeLogin方法
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        // 获取请求token
        String token = httpServletRequest.getHeader("token");
        // StringUtils.isBlank(String str)  判断str字符串是否为空或者长度是否为0
        if (token == null || "".equals(token)) {
            httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
            httpServletResponse.setHeader("Access-Control-Allow-Origin", httpServletRequest.getHeader("Origin"));
            httpServletResponse.setCharacterEncoding("UTF-8");
            Response msg = Response.fail("请先登录");
            httpServletResponse.getWriter().write(JSON.toJSONString(msg));
            return false;
        }
        return executeLogin(request, response);
    }

    // token失效时调用
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        httpResponse.setContentType("application/json;charset=utf-8");
        httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
        httpResponse.setHeader("Access-Control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpResponse.setCharacterEncoding("UTF-8");
        try {
            //处理登录失败的异常
            Throwable throwable = e.getCause() == null ? e : e.getCause();
            Response msg = Response.fail("登录凭证已失效,请重新登录");
            httpResponse.getWriter().write(JSON.toJSONString(msg));
        } catch (IOException e1) {

        }
        return false;
    }


}
  • jwt util
public class JwtUtils {

    /**
     * 签发对象:这个用户的id
     * 签发时间:现在
     * 有效时间:30分钟
     * 载荷内容:暂时设计为:这个人的名字,这个人的昵称
     * 加密密钥:这个人的id加上一串字符串
     */
    public static String createToken(String userId, String realName, String userName) {

        Calendar nowTime = Calendar.getInstance();
        nowTime.add(Calendar.MINUTE, 30);
        Date expiresDate = nowTime.getTime();
        return JWT.create()
                // 签发对象
                .withAudience(userId)
                // 发行时间
                .withIssuedAt(new Date())
                // 有效时间
                .withExpiresAt(expiresDate)
                // 载荷,随便写几个都可以
                .withClaim("userName", userName)
                .withClaim("realName", realName)
                // 加密
                .sign(Algorithm.HMAC256(userId + "HelloLehr"));
    }

    /**
     * 检验合法性,其中secret参数就应该传入的是用户的id
     *
     * @param token
     */
    public static void verifyToken(String token, String secret) {
        DecodedJWT jwt = null;
        try {
            JWTVerifier verifier = JWT.require(Algorithm.HMAC256(secret + "HelloLehr")).build();
            jwt = verifier.verify(token);
        } catch (Exception e) {
            // 效验失败
            // 这里抛出的异常是我自定义的一个异常,你也可以写成别的
            throw new TokenUnavailableException();
        }
    }

    /**
     * 获取签发对象
     */
    public static String getAudience(String token) {
        String audience = null;
        try {
            audience = JWT.decode(token).getAudience().get(0);
        } catch (JWTDecodeException j) {
            // 这里是token解析失败
            throw new TokenUnavailableException();
        }
        return audience;
    }

    /**
     * 通过载荷名字获取载荷的值
     */
    public static Claim getClaimByName(String token, String name) {
        return JWT.decode(token).getClaim(name);
    }

}

jwt token

public class JwtToken extends UsernamePasswordToken {

    String token;

    public JwtToken(String token) {
        this.token = token;
    }

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

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

有了以上类,基本上计算实现了,接下来写登录

5. 登录

login controller

@RestController
public class LoginController {

    @Autowired
    private SysUserMapper sysUserMapper;

    @PostMapping("/login")
    public Response login(@RequestParam("username") String username, @RequestParam("password") String password) {
        QueryWrapper<SysUser> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("username", username);
        SysUser sysUser = sysUserMapper.selectOne(queryWrapper);
        if (sysUser == null) {
            return Response.fail("账号或密码错误");
        }
        if (!sysUser.getPwd().equals(password)) {
            return Response.fail("账号或密码错误");
        }
        String token = JwtUtils.createToken(sysUser.getId().toString(), sysUser.getUsername(), sysUser.getUsername());
        return Response.success("登录成功").add("token", token);
    }

}

6. 测试controller

@RestController
@RequestMapping("/menu")
@Slf4j
public class MenuController {

    @Autowired
    private MenuService menuService;

    @GetMapping("/view")
    @RequiresPermissions("1")
    public Response view() {
        return Response.success("success").add("menu", menuService.list());
    }

    @GetMapping("/add")
    @RequiresPermissions("2")
    public Response add() {
        log.info("menu add.....");
        return Response.success("success");
    }

}

相关文章

网友评论

      本文标题:spring boot 整合shiro 、jwt

      本文链接:https://www.haomeiwen.com/subject/ftmapltx.html