spring boot 整合shiro 、jwt

参考:
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");
    }

}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,923评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,154评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,775评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,960评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,976评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,972评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,893评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,709评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,159评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,400评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,552评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,265评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,876评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,528评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,701评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,552评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,451评论 2 352

推荐阅读更多精彩内容