前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >shiro实战之改造成token格式的无状态restful api

shiro实战之改造成token格式的无状态restful api

作者头像
山行AI
发布2019-08-13 23:00:41
5.4K2
发布2019-08-13 23:00:41
举报
文章被收录于专栏:山行AI

rest风格的api一般是使用oauth2协议或者是rest + jwt模式,我们这里使用的是后者。

改造过程主要分为以下几步:

  • 禁用shiro session
  • jwt生成token与校验token
  • 自定义shiro token
  • 自定义realm中授权和认证方法的改造
  • 自定义filter中的isAccessAllowed和onAccessDenied方法的改造
  • 配置类改造

需要注意的是,登录操作的模式是不变的。 禁用shiro session

代码语言:javascript
复制
public class StatelessDefaultSubjectFactory extends DefaultWebSubjectFactory {      public Subject createSubject(SubjectContext context) {          //不创建session        context.setSessionCreationEnabled(false);          return super.createSubject(context);      }  }

通过调用context.setSessionCreationEnabled(false)表示不创建会话;如果之后调用Subject.getSession()将抛出DisabledSessionException异常。

jwt生成token和校验token

代码语言:javascript
复制
 /**     * 从数据声明生成令牌     *     * @param claims 数据声明     * @return 令牌     */    public static String generateToken(Map<String, Object> claims) {        Date expirationDate = new Date(System.currentTimeMillis() + EXPIRE_TIME);        return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, SECRET).compact();    }  /**      * 从令牌中获取用户名      *      * @param token 令牌      * @return 用户名      */     public static String getUsernameFromToken(String token) {         Claims claims = getClaimsFromToken(token);         return claims.getSubject();     }     /**      * 验证令牌      *      * @param token      * @param username      * @return      */     private static Boolean validateToken(String token, String username) {         String userName = getUsernameFromToken(token);         return (userName.equals(username) && !isTokenExpired(token));     }     /**      * 从令牌中获取数据声明      *      * @param token 令牌      * @return 数据声明      */     private static Claims getClaimsFromToken(String token) {         Claims claims;         try {             claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();         } catch (Exception e) {             claims = null;         }         return claims;     }     ...      

主要有生成token和校验token的方法。

自定义token

代码语言:javascript
复制
public class StatelessToken implements AuthenticationToken {    private String username;    private String token;    public StatelessToken(String username, String token) {        this.username = username;        this.token = token;    }    @Override    public Object getPrincipal() {        return username;    }    @Override    public Object getCredentials() {        return token;    }}

自定义realm中授权和认证方法的改造

代码语言:javascript
复制
public class StatelessRealm extends AuthorizingRealm {    @Override    public boolean supports(AuthenticationToken token) {        //仅支持StatelessToken类型的Token        return token instanceof StatelessToken;    }    @Override    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {        //根据用户名查找角色,请根据需求实现        String username = (String) principals.getPrimaryPrincipal();        //TODO 根据用户查找角色,根据自己的业务实现,这里角色硬编码为admin        SimpleAuthorizationInfo authorizationInfo =  new SimpleAuthorizationInfo();        authorizationInfo.addRole("admin");        return authorizationInfo;    }    @Override    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {        StatelessToken statelessToken = (StatelessToken) token;        String username = (String) statelessToken.getPrincipal();        //生成token,这里生成token的方式依实际情况而定,可以定义得复杂些        String generateToken = JwtUtil.generateToken(ImmutableMap.of(username,username));        //然后进行客户端消息摘要和服务器端消息摘要的匹配        return new SimpleAuthenticationInfo(                username,                generateToken,                getName());    }}

关于doGetAuthenticationInfo的返回的SimpleAuthenticationInfo分析如下:

  1. StatelessRealm的父类构造方法:
代码语言:javascript
复制
public AuthenticatingRealm() {        this(null, new SimpleCredentialsMatcher());    }

使用的是SimpleCredentialsMatcher。

2.父类中调用doGetAuthenticationInfo方法的地方:

这里需要注意以下几点:

  • 是有缓存的,先尝试从缓存中拿,所以如果有缓存条件下有权限变更之类的操作需要刷新缓存。
  • 传给assertCredentialsMatch方法的是token和自定义realm中返回的SimpleAuthenticationInfo

3.org.apache.shiro.realm.AuthenticatingRealm#assertCredentialsMatch:

这里使用的是SimpleCredentialsMatcher,我们看下它的doCredentialsMatch方法:

代码语言:javascript
复制
 public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {        Object tokenCredentials = getCredentials(token);        Object accountCredentials = getCredentials(info);        return equals(tokenCredentials, accountCredentials);    }protected Object getCredentials(AuthenticationToken token) {        return token.getCredentials();    } protected Object getCredentials(AuthenticationInfo info) {        return info.getCredentials();    }
  • AuthenticationToken为传入的StatelessToken,它的getCredentials()是上面自己实现的。
  • SimpleAuthenticationInfo的构造方法:
代码语言:javascript
复制
 public SimpleAuthenticationInfo(Object principal, Object credentials, String realmName) {        this.principals = new SimplePrincipalCollection(principal, realmName);        this.credentials = credentials;    }

可见,this.credentials为第二个入参。

自定义filter的改造

它的onAccessDenied方法改造如下:

代码语言:javascript
复制
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {        HttpServletRequest req = (HttpServletRequest) request;        String jwt = req.getHeader("Authorization");        //可以使用userid        String username = req.getHeader("username");        if (JwtUtil.validateToken(jwt)) {            StatelessToken statelessToken = new StatelessToken(username, jwt);            try {                //委托realm进行登录认证                getSubject(request, response).login(statelessToken);                return true;            }catch (Exception e) {                return false;            }        }        redirectToLogin(request,response);        return false;    }

除了从header中获取外,还可以从cookie中存取。

这里需要注意一个问题,当token被人窃取之后,放入header就能无限制登录了。解决办法主要是像oauth2那样加入refreshToken机制,具体的请自行查看之前关于oauth2的推文。

配置类改造

代码语言:javascript
复制
    /**     * Shiro的Web过滤器Factory 命名:shiroFilter     */    @Bean(name = "shiroFilter")    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();        //Shiro的核心安全接口,这个属性是必须的        shiroFilterFactoryBean.setSecurityManager(securityManager);        Map<String, Filter> filterMap = new LinkedHashMap<>();        filterMap.put("authc", new AjaxPermissionsAuthorizationFilter());        shiroFilterFactoryBean.setFilters(filterMap);        /*定义shiro过滤链  Map结构         * Map中key(xml中是指value值)的第一个'/'代表的路径是相对于HttpServletRequest.getContextPath()的值来的         * anon:它对应的过滤器里面是空的,什么都没做,这里.do和.jsp后面的*表示参数,比方说login.jsp?main这种         * authc:该过滤器下的页面必须验证后才能访问,它是Shiro内置的一个拦截器org.apache.shiro.web.filter.authc.FormAuthenticationFilter         */        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();         /* 过滤链定义,从上向下顺序执行,一般将 / ** 放在最为下边:这是一个坑呢,一不小心代码就不好使了;          authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问 */        filterChainDefinitionMap.put("/", "anon");        filterChainDefinitionMap.put("/static/**", "anon");        filterChainDefinitionMap.put("/login/auth", "anon");        filterChainDefinitionMap.put("/login/logout", "anon");        filterChainDefinitionMap.put("/error", "anon");        filterChainDefinitionMap.put("/**", "authc");        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);        return shiroFilterFactoryBean;    }    @Bean    public SubjectFactory subjectFactory(){        SubjectFactory subjectFactory = new StatelessDefaultSubjectFactory();        return subjectFactory;    }    /**     * 不指定名字的话,自动创建一个方法名第一个字母小写的bean     */    @Bean    public SecurityManager securityManager() {        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();        securityManager.setRealm(userRealm());        securityManager.setSubjectFactory(subjectFactory());        return securityManager;    }

其他方法

加入拦截器

对加上了AuthPassport注解的接口进行拦截,对header或cookie中的token进行有效性校验,或者是refreshToken类似的校验,不符合要求的直接拦截掉。

和shiro一起使用时需要注意灵活应用。因为shiroFilter属于filter它的执行顺序在interceptor之前。

参考

  1. https://blog.csdn.net/weixin_42058600/article/details/81837056
  2. https://jinnianshilongnian.iteye.com/blog/2041909
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-08-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 开发架构二三事 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • jwt生成token和校验token
  • 自定义token
  • 自定义realm中授权和认证方法的改造
  • 自定义filter的改造
  • 配置类改造
  • 其他方法
    • 加入拦截器
    • 参考
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档