springboot+shiro+jwt+mybatiesplus前后端分离项目快速入门(二,shiro入门)

springboot+shiro+jwt+mybatiesplus前后端分离项目快速入门(二.shiro实战)
传送门>github地址,main分支为项目搭建即第一篇内容,complete分支为整合后的完整代码

1.名词解释

解释重要性
subject当前主体
SecurityManager管理subject的安全认证管理者
realm只做两件事,帮助securityManager’‘认证’‘和’‘授权’’

2.配置shiro

2.1. 创建realm

@Slf4j
@Configuration
public class MyRealm extends AuthorizingRealm {
    @Autowired
    private AccountService adminService;
    @Autowired
    private RoleService roleService;
    @Autowired
    private PermissionService permissionService;
    
    /**
     * 用来做授权   只管理权限,查表的地方
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        log.info("---------------- 执行 Shiro 权限获取 ---------------------");
        //从Shiro中获取用户名
        Subject subject = SecurityUtils.getSubject();
        Account currentAccount = (Account) subject.getPrincipal(); // 因为认证时principal用的是account对象,所以可以直接取出来强转
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        // 数据库查询权限
        Set<Permission> jurisdictionByAccountName = permissionService.getJurisdictionByAccountName(currentAccount.getAccountName());
        Set<String> jurisdictions = jurisdictionByAccountName.stream().map(Permission::getUri).collect(Collectors.toSet());
        log.info("当前用户:{} 拥有权限:{}", currentAccount.getAccountName(), jurisdictions.toString());
        //设置accountName拥有的权限
        authorizationInfo.addStringPermissions(jurisdictions);
        //设置accountName拥有的角色
        Set<String> roles = roleService.getRolesByAccountName(currentAccount.getAccountName());
        authorizationInfo.setRoles(roles);
        return authorizationInfo;
    }

    /**
     * 用来做认证   校验账号密码是否正确的地方
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        log.debug("===============Shiro身份认证开始============doGetAuthenticationInfo==========");
        //将AuthenticationToken强转成UsernamePasswordToken 这样获取账号和密码更加的方便
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        Object credentials = authenticationToken.getCredentials();
        //获取用户在浏览器中输入的账号
        String userName = token.getUsername();
        String password = new String(token.getPassword());
        //认证账号,正常情况我们需要这里从数据库中获取账号的信息,以及其他关键数据,例如账号是否被冻结等等
        Account admin = adminService.getOne(new LambdaQueryWrapper<Account>().eq(Account::getAccountName, userName));
        if (admin == null)  throw new UnknownAccountException("账号不存在");//判断用户账号是否存在
        if (admin.getStatus() == -1) throw new LockedAccountException("账号被冻结");
        if (!admin.getAccountPassword().equals(password)) throw new IncorrectCredentialsException("账号或密码错误");
        //密码认证  shiro做
        return new SimpleAuthenticationInfo(admin, credentials, getName());
    }
}

2.2 重写shiro的过滤器

2.2.1 创建UrlPermissionFilter

目的是完成解析token的操作

@Slf4j
public class UrlPermissionFilter extends AccessControlFilter {
    /**
     * 表示是否允许访问 ,如果允许访问返回true,否则false; 这里的o参数 有兴趣的可以研究讨论下
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
        Subject subject = getSubject(servletRequest, servletResponse);
        String url = getPathWithinApplication(servletRequest);
        if ("/error".equals(url)) return true;
        log.info("当前用户正在访问的 url => " + url);
        // 先判断一下是不是登录  验证token
        HttpServletRequest servletRequest1 = (HttpServletRequest) servletRequest;
        String token = servletRequest1.getHeader("Authorization");
        if (!"/login".equals(url) && StrUtil.isEmpty(token)) {
            IdentityException identityException = new IdentityException("请求头缺少或携带无效标识:Authorization");
            servletRequest.setAttribute("noneAuthorization",identityException);
            throw identityException;
        }
        if (StrUtil.isNotEmpty(token)) {
            if (!SecureUtil.authUser(token)) throw new RuntimeException("token无效错误");
        }
        return subject.isPermitted(url);
    }

    /**
     * onAccessDenied:表示当访问拒绝时是否已经处理了; 如果返回 true 表示需要继续处理; 如果返回 false
     * 表示该拦截器实例已经处理了,将直接返回即可。
     */
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        HttpServletRequest servletRequest1 = (HttpServletRequest) servletRequest;
        String msg = SecurityUtils.getSubject().isAuthenticated() ? "当前账户无接口["+servletRequest1.getRequestURI()+"]权限操作" : "账户登录态失效,请在登录后重试";
        IdentityException exception = new IdentityException(msg);
        servletRequest1.setAttribute("identityException", exception);
        throw new RuntimeException(exception);// 这里是使用过滤器坑爹的地方 暂且这样抛出异常后续有解释
    }
}

2.3 创建UrlPermission

@Configuration
@Setter
public class UrlPermission implements Permission {
    private static final Logger logger = LoggerFactory.getLogger(UrlPermission.class);
    // 在 Realm 的授权方法中,由数据库查询出来的权限字符串
   private String url;
    public UrlPermission(String url) {
        this.url = url;
    }
    public UrlPermission() {
    }
    /**
     * 一个很重要的方法,用户判断 Realm 中设置的权限和从数据库或者配置文件中传递进来的权限信息是否匹配
     * 如果 Realm 的授权方法中,一个认证主体有多个权限,会进行遍历,直到匹配成功为止
     * this.url 是在遍历状态中变化的
     * urlPermission.url 是从 subject.isPermitted(url)
     * 传递到 UrlPermissionResolver 中传递过来的,就一个固定值
     */
    @Override
    public boolean implies(Permission permission) {
        if (!(permission instanceof UrlPermission)) {
            return false;
        }
        UrlPermission urlPermission = (UrlPermission) permission;
        PatternMatcher patternMatcher = new AntPathMatcher();
        logger.info("this.url(来自数据库中存放的通配符数据),在 Realm 的授权方法中注入的 => " + this.url);
        logger.info("urlPermission.url(来自浏览器正在访问的链接) => " + urlPermission.url);
        boolean matches = patternMatcher.matches(this.url, urlPermission.url);
        logger.info("matches => " + matches);
        return matches;
    }
}

2.4 创建 角色-权限 & url权限 解析器

/**
*有点多余,暂时没想出来解决办法
*/
public class RolePermissionResolver implements org.apache.shiro.authz.permission.RolePermissionResolver {
    @Autowired
    RoleService roleService;
    @Override
    public Collection<Permission> resolvePermissionsInRole(String roleString) {
        Set<RoleNameJriDo> jurisdictionsByRoleName = roleService.getJurisdictionsByRoleName(roleString);
        Collection<Permission> permissions =new ArrayList<>();
        for (RoleNameJriDo roleNameJriDo : jurisdictionsByRoleName) {
            if ((roleString+"").equals(roleNameJriDo.getRoleName())){
                UrlPermission urlPermission = new UrlPermission(roleNameJriDo.getUri());
                permissions.add(urlPermission);
            }
        }
        return permissions;
    }
}
public class UrlPermissionResolver implements PermissionResolver {
    private static final Logger logger = LoggerFactory.getLogger(UrlPermissionResolver.class);
    /**
     * 经过调试发现
     * subject.isPermitted(url) 中传入的字符串
     * 和自定义 Realm 中传入的权限字符串集合都要经过这个 resolver
     */
    @Override
    public Permission resolvePermission(String s) {
        logger.info("s => " + s);
        if(s.startsWith("/")) return new UrlPermission(s);
        return new WildcardPermission(s);
    }
}

2.6 自定义一个身份鉴定异常

public class IdentityException extends RuntimeException{
    public IdentityException(String message) {
        super(message);
    }
}

2.5 创建ShiroConfig类(很重要)

@Configuration
@Slf4j
public class ShiroConfig {
    /**
     * 配置Shiro的安全管理器
     */
    @Bean
    public DefaultWebSecurityManager securityManager(Realm myRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //设置一个Realm,这个Realm是最终用于完成我们的认证号和授权操作的具体对象
        securityManager.setRealm(myRealm);
        return securityManager;
    }
    
    /**
     * 配置一个自定义的Realm的bean,最终将使用这个bean返回的对象来完成我们的认证和授权
     */
    @Bean
    public MyRealm myRealm() {
        MyRealm myRealm = new MyRealm();
        myRealm.setPermissionResolver(urlPermissionResolver());
        myRealm.setRolePermissionResolver(rolePermissionResolver());
        return myRealm;
    }
    // 两个解析器
    @Bean
    public UrlPermissionResolver urlPermissionResolver() {
        return new UrlPermissionResolver();
    }

    @Bean
    public RolePermissionResolver rolePermissionResolver() {
        return new RolePermissionResolver();
    }

    /**
     * 配置一个Shiro的过滤器bean,这个bean将配置Shiro相关的一个规则的拦截
     * 如什么样的请求可以访问,什么样的请求不可以访问等等
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);
        /**
         * 覆盖默认的user拦截器(默认拦截器解决不了ajax请求 session超时的问题,若有更好的办法请及时反馈作者)
         */
        HashMap<String, Filter> myFilters = new HashMap<>();
        UrlPermissionFilter uriFilter = new UrlPermissionFilter();
        myFilters.put("urlPermissionFilter", uriFilter);//自定义拦截器
        shiroFilter.setFilters(myFilters);//添加自定义拦截器

        /**
         * 配置shiro拦截器
         * anon  不需要认证
         * authc 需要认证
         * user  验证通过或RememberMe登录的都可以
         * 当应用开启了rememberMe时,用户下次访问时可以是一个user,但不会是authc,因为authc是需要重新认证的
         * 顺序从上到下,优先级依次降低
         */
        Map<String, String> hashMap = new LinkedHashMap<>();
        hashMap.put("/login", "anon");
        hashMap.put("/", "anon");
        hashMap.put("/**", "urlPermissionFilter");//anon authc 是shiro自带的内部filter,由于我们重写了filter,所以配置所有uri走我们的filer
        shiroFilter.setFilterChainDefinitionMap(hashMap);
        return shiroFilter;
    }
    /**
     * Shiro生命周期处理器
     */
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }
        /**
     * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions)
     * 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可实现此功能
     * @return
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
}

3.创建controller使用shiro

3.1 ShiroController

@RestController
@RequestMapping("/")
public class ShiroController {
    @PostMapping("login")
    public String login(@RequestBody Account account) {
        // 验证账号密码
        //创建一个shiro的Subject对象,这个对象始于登录接口,验证通过后shiro保存正确的账号密码,后面再次登录将账号密码封装好token(可不是jwt的密文token)对象扔给shiro做比对即可
        Subject subject = SecurityUtils.getSubject();
        //创建一个用户账号和密码的Token对象,并设置用户输入的账号和密码
        UsernamePasswordToken token = new UsernamePasswordToken(account.getAccountName(), account.getAccountPassword());
        try {
            //调用login后,Shiro就会自动执行自定义的Realm中的认证方法
            subject.login(token);
            Session session = subject.getSession();
            if (session != null) session.setTimeout(1000 * 60); // 非常关键 这里是shiro缓存session判断登录状态的失效时间,与token失效时间一致即可,不做扩展,如果只想验证token可以传-1跳过认证,只识别token
            return "登录成功" + SecureUtil.mkToken(account.getAccountName());
        } catch (UnknownAccountException e) {
            //表示用户的账号错误,myRealm的认证方法抛出
            return "账号不存在";
        } catch (LockedAccountException e) {
            //表示用户的账号被锁定,myRealm的认证方法抛出
            return "账号被锁定";
        } catch (IncorrectCredentialsException e) {
            //表示用户的密码错误,myRealm的认证方法抛出
            return "账号或密码错误";
        }
    }

    @GetMapping("testApi")
    public String testApi() {
        return "testApi正常访问";
    }
}

3.2 ErrorControllerImpl

过滤器异常不会到达全局异常处理器,会重定向至错误页面/error,如果不处理峨口返回格式将不完整,所以对/error路径进行处理

@Controller
public class ErrorControllerImpl implements ErrorController {
    @Override
    public String getErrorPath() {
        return "/error";
    }
    @RequestMapping("/error")
    public void handleError(HttpServletRequest request) throws Throwable {
        // Attribute在过滤器里已经传递过了,所以直接强转抛出
        if (request.getAttribute("identityException") != null) {
            throw (IdentityException) request.getAttribute("identityException");
        }
        if (request.getAttribute("noneAuthorization") != null) {
            throw (IdentityException) request.getAttribute("noneAuthorization");
        }
    }
}

3.3使用hutool封装的jwt工具生成token

public class SecureUtil {
    public static final String ACCOUNTNAME = "accountName";   //账号名
    private static final String AUTH_SECRET = "SEDFGEGEWRWEWYHDG"; //jwt验签盐

    // 生成token
    public static final String mkToken(String accountName) {
        Map<String, Object> map = new HashMap<String, Object>() {
            private static final long serialVersionUID = 1L;
            {
                put(ACCOUNTNAME, accountName);
                put("TIMESTAMP", System.currentTimeMillis());
            }
        };
        return JWTUtil.createToken(map, AUTH_SECRET.getBytes());
    }

    // 验证身份
    public static boolean authUser(String token) {
        try {
            if (StrUtil.isEmpty(token)) return false;
            if (!JWTUtil.verify(token, AUTH_SECRET.getBytes())) return false;
            Map<String, String> authInfo = SecureUtil.getAuthInfo(token);
            if (StrUtil.isEmpty(authInfo.get(ACCOUNTNAME))) return false;
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    // 解析信息
    public static Map<String, String> getAuthInfo(String token) {
        final JWT jwt = JWTUtil.parseToken(token);
        String accountName = jwt.getPayload(ACCOUNTNAME) == null ? null : jwt.getPayload(ACCOUNTNAME).toString();
        Map<String, String> map = new HashMap();
        map.put(ACCOUNTNAME, accountName);
        return map;
    }

    // 解析token  取出accountName
    public static String getAccountName(String token) {
        Map<String, String> authInfo = getAuthInfo(token);
        if (authInfo.isEmpty() || StrUtil.isEmpty(authInfo.get(ACCOUNTNAME)))
            throw new RuntimeException("token未找到accountName");
        return authInfo.get(ACCOUNTNAME);
    }
}

4.shiro配置目录总览

在这里插入图片描述

5.开始测试

- 测试样例1 未携带jwt-token访问/testApi接口

在这里插入图片描述

  • 测试样例2 携带jwt-token访问/testApi接口

在这里插入图片描述

这里需要提醒的是,在/login接口中,是指定了登录过期时间的,这个时间是shiro管理的,所以生成token也要定时失效,本实力项目为精简暂未做到统一,请注意,当然直接设置-1可以永久保持登录状态,看项目需求了
关键代码:

  if (session != null) session.setTimeout(1000 * 60); // 非常关键 这里是shiro缓存session判断登录状态的失效时间,与token失效时间一致即可,不做扩展
  • 测试样例3 登录并获取token

在这里插入图片描述

密码错误,账号冻结等情况还请君自测

  • 测试样例4 重新测样例2

在这里插入图片描述

  • 测试样例5 给root账号分配访问 /testApi的权限

执行sql给root账户分配接口访问权限:

INSERT INTO account_role(account_id,role_id)VALUES (1,1);
INSERT INTO role(role_name) VALUES ('管理员');
-- 这里的uri,可以是自定义的任何uri
INSERT INTO permission(permission_portrayal,uri)VALUES('访问aipTest接口','/testApi');
INSERT INTO role_permission(role_id,permission_id)VALUES(1,1);

再次访问接口
在这里插入图片描述

  • 测试样例5 使用注解限制权限

改造testApi:

    @RequiresRoles("sysAdmin")
    @GetMapping("testApi")
    public String testApi() {
        return "testApi正常访问";
    }

同类注解还有以下注解,请参考其他资料

@RequiresRoles
@RequiresPermissions
@RequiresAuthentication
@RequiresUser
@RequiresGuest

再次请求
在这里插入图片描述
完毕!!!

6.待优化

  • :集成redis缓存
  • rememberMe功能

如有疑问请留言,看到必回复

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值