转载

珠联璧合 | ServiceComb 集成 Shiro 实践

Shiro简介

Apache Shiro 是一款功能强大、易用的轻量级开源 Java 安全框架,它主要提供认证、鉴权、加密和会话管理等功能。 Spring Security 可能是业界用的最广泛的安全框架,但是 Spring Security Spring 耦合的太重,脱离了 Spring 框架就使用不了,所以一个轻量级的安全框架有时也是一个非常不错的选择。

Shiro 主要通过安全 API 来提供四个方面使用:

  1. 认证 Authentication – 提供用户身份,可以理解为登录验证。

  2. 授权 Authorization – 访问控制,也就是通常所讲 ACL Access Control List )的 RBAC Role Base Access Control )或者 ABAC(Attribute Base Access Control)

  3. 加密 Cryptography – 加密、保护数据,确保数据安全。

  4. 会话管理 Session Management – 登录后的会话管理, Shiro 有独立的会话管理机制,可以是 J2EE 的会话,也可以是普通 Java 应用的。

珠联璧合 | ServiceComb 集成 Shiro 实践

Shiro 有几个关键的核心概念: Subject SecurityManager Realms ,我们简单的介绍下这几个概念的含义:

Subject

权限责任主体,主要是让系统识别要管理的对象,比如一般系统的用户,这个也不一定是人,也可以是一台设备,Subject有登录、注销、权限检测等操作。所有的Subject都会绑定到SecurityManager上面,所有Subject的交互都会委托给SecurityManager。

SecurityManager

安全管理器,所有和安全相关的操作都会与SecurityManager打交道,它管理着所有的Subject,它就是Shiro的架构核心

Realm

领域,Shiro从Realm中获取安全数据。Realm扮演者Shiro和应用之间的桥梁,比如用户、角色列表。应用可以自定义实现不同的Realm,Shiro也提供了几个开箱即用的Realm,比如SimpleAccountRealm、IniRealm、JdbcRealm和DefaultLdapRealm、JndiRealm。通过这些简单的Realm我们可以很简单的上手Shiro,基本上所有定制化的扩展点都在实现自定义的Realm。

珠联璧合 | ServiceComb 集成 Shiro 实践

既然 Shiro 可以提供如此全面、简单易用的安全权限功能,那么 ServiceComb 是不是也可以非常方便的来进行集成呢?

答案当然是可以了。

简单集成

ServiceComb 集成 Shiro ,可以使用两种方案,一种是集成 Vertx-shiro ,使用这种方法前提是使用 Rest over Vertx Transport 方式,另外一种就是使用 ServiceComb handler 或者 HttpServerFilter 扩展点机制。

第一种方式优点是可以使用异步的方式,完全使用 vertx 的扩展机制,跟 ServiceComb 关联不大,只需要扩展实现一个 org.apache.servicecomb.transport.rest.vertx.VertxHttpDispatcher ,在 init 方法中把认证逻辑加到要过滤的 URL 上。

1、   POM 中引入 vertx-shiro 依赖

<dependency>

<groupId>io.vertx</groupId>

<artifactId>vertx-auth-shiro</artifactId>

<version>3.6.3</version>

</dependency>

2、   增加 vertx-shiro 的用户、角色配置文件 test-auth.properties

user.root = rootPassword,administrator

user.jsmith = jsmithPassword,manager,engineer,employee

user.abrown = abrownPassword,qa,employee

user.djones = djonesPassword,qa,contractor

user.test = testPassword,qa,contractor


role.administrator = *

role.manager = "user:read,write", file:execute:/usr/local/emailManagers.sh

role.engineer = "file:read,execute:/usr/local/tomcat/bin/startup.sh"

role.employee = application:use:wiki

role.qa = "server:view,start,shutdown,restart:someQaServer", server:view:someProductionServer

role.contractor = application:use:timesheet

3、   扩展实现 VertxHttpDispatcher

package com.service.servicecombshiro;


import org.apache.servicecomb.foundation.vertx.VertxUtils;

import org.apache.servicecomb.transport.rest.vertx.VertxRestDispatcher;


import io.vertx.core.Vertx;

import io.vertx.core.json.JsonObject;

import io.vertx.ext.auth.AuthProvider;

import io.vertx.ext.auth.User;

import io.vertx.ext.auth.shiro.ShiroAuth;

import io.vertx.ext.auth.shiro.ShiroAuthOptions;

import io.vertx.ext.auth.shiro.ShiroAuthRealmType;

import io.vertx.ext.web.Router;


public class AuthVertxHttpDispatcher extends VertxRestDispatcher {


@Override

public boolean enabled() {

return true;

}


@Override

public int getOrder() {

return 0;

}


@Override

public void init(Router router) {

JsonObject config = new JsonObject().put("properties_path", "classpath:test-auth.properties");

Vertx vertx = VertxUtils.getVertxMap().get("transport");

AuthProvider authProvider = ShiroAuth

.create(vertx, new ShiroAuthOptions().setType(ShiroAuthRealmType.PROPERTIES).setConfig(config));


router.route().handler(rc -> {

JsonObject authInfo = new JsonObject().put("username", "test").put("password", "testPassword");

authProvider.authenticate(authInfo, res -> {

if (res.failed()) {

// Failed!

rc.response().setStatusCode(401).end("No right!");

return;

}

User user = res.result();

System.out.println(user.principal());

rc.next();

});

});

}

}

第二种方式就是使用扩展点的机制,示例中使用 HttpServerFilter 扩展点机制,所有的 REST 请求都会走到 HttpServerFilter 逻辑。具体实现如下:

1、   引入 shiro 的依赖

<dependency>

<groupId>org.apache.shiro</groupId>

<artifactId>shiro-core</artifactId>

<version>1.4.1</version>

</dependency>

2、   定义 shiro 的用户信息文件 src/main/resources/shiro.ini 文件

[users]

admin=123456

user1=Test123456

3、   使用 SPI 机制实现一个 HttpServerFilter 来做身份认证,这个简单的示例我们使用 Http   Basic Auth 的认证方式来实现基本的身份认证。首先要初始化一个 SecurityManager ,并注入一个 Realm ,然后在 afterReceiveRequest 方法中获取身份信息,并且对身份信息做校验。(由于 Shiro 当前很多实现都是使用了线程上下文来传递 SecurityManager ,所以本实例只能使用同步编码的方式)

package com.service.servicecombshiro.auth;


import org.apache.servicecomb.common.rest.filter.HttpServerFilter;

import org.apache.servicecomb.core.Invocation;

import org.apache.servicecomb.foundation.vertx.http.HttpServletRequestEx;

import org.apache.servicecomb.swagger.invocation.Response;

import org.apache.shiro.SecurityUtils;

import org.apache.shiro.authc.AuthenticationException;

import org.apache.shiro.authc.UsernamePasswordToken;

import org.apache.shiro.codec.Base64;

import org.apache.shiro.mgt.DefaultSecurityManager;

import org.apache.shiro.realm.Realm;

import org.apache.shiro.realm.text.IniRealm;

import org.apache.shiro.subject.Subject;


public class HttpAuthFilter implements HttpServerFilter {


private org.apache.shiro.mgt.SecurityManager securityManager;


public HttpAuthFilter() {

Realm realm = new IniRealm("classpath:shiro.ini"); //使用ini的配置方法来初始化Realm

this.securityManager = new DefaultSecurityManager(realm); //初始化SecurityManager

}


@Override

public int getOrder() {

return -10000; // 确保这个Filter在一般的filter之前先执行

}


@Override

public Response afterReceiveRequest(Invocation invocation, HttpServletRequestEx httpServletRequestEx) {

SecurityUtils.setSecurityManager(securityManager); // 因为用到了线程上下文,只支持同步编码方式

Subject user = SecurityUtils.getSubject();

String userInfo = httpServletRequestEx.getHeader("Authorization");

if (userInfo == null || userInfo.isEmpty()) {

return Response.create(401, "Unauthorized",

"WWW-Authenticate: Basic realm=protected_docs");

}

if (userInfo.length() < 5 || !userInfo.startsWith("Basic")) {

return Response.create(401, "Unauthorized",

"Header is wrong!");

}

String authInfo = userInfo.substring(5).trim();

String[] authInfos = Base64.decodeToString(authInfo).split(":");

if (authInfos.length != 2) {

return Response.create(401, "Unauthorized",

"Header is wrong!");

}

UsernamePasswordToken token = new UsernamePasswordToken(authInfos[0], authInfos[1]); // 获取到请求的用户名和密码

String path = httpServletRequestEx.getPathInfo();

if (path.startsWith("/auth")) { // 只对特定的资源检测

try {

user.login(token); // 登录不报异常表示成功了

} catch (AuthenticationException e) {

System.out.println("Has no right!"); // 异常表示身份认证失败

return Response.create(401, "Unauthorized", e.getMessage());

}

}

return null;

}

}

4、   发送请求进行验证

curl -X GET 'http://127.0.0.1:8080/auth/helloworld?name=test' -H 'authorization: Basic YWRtaW46MTIzNDU2'

珠联璧合 | ServiceComb 集成 Shiro 实践

分布式集成

微服务化的系统中,应用一般都是无状态的,所以服务器端一般不会实现传统的 J2EE 容器的会话机制,而是使用外置会话、 Oath2 协议,也可以使用无会话方案,每次请求客户端都带上身份信息,服务端都对客户端的身份进行识别,这种方案典型实现就是 JWT

珠联璧合 | ServiceComb 集成 Shiro 实践

1、   引入 JWT Shiro 依赖

<dependency>

<groupId>org.apache.shiro</groupId>

<artifactId>shiro-core</artifactId>

<version>1.4.1</version>

</dependency>

<dependency>

<groupId>com.auth0</groupId>

<artifactId>java-jwt</artifactId>

<version>3.8.2</version>

</dependency>

2、   定义 shiro 的用户配置文件 src/main/resources/shiro.ini

[users]

admin=123456

user1=Test123456

3、   实现一个 JWTUtils ,主要用来做 JWT   Token 的签名和校验

package com.service.servicecombshiro.auth;


import java.util.Date;


import org.slf4j.Logger;

import org.slf4j.LoggerFactory;


import com.auth0.jwt.JWT;

import com.auth0.jwt.algorithms.Algorithm;

import com.auth0.jwt.exceptions.JWTCreationException;

import com.auth0.jwt.exceptions.JWTDecodeException;

import com.auth0.jwt.exceptions.JWTVerificationException;

import com.auth0.jwt.interfaces.DecodedJWT;

import com.auth0.jwt.interfaces.JWTVerifier;


public class JWTUtils {

private static final Logger LOGGER = LoggerFactory.getLogger(JWTUtils.class);


private static final int TOKEN_VALID_TIME = 5 * 60 * 1000;


public static boolean verify(String username, String secret, String token) {

try {

Algorithm algorithm = Algorithm.HMAC256(secret);

JWTVerifier verifier = JWT.require(algorithm)

.withClaim("username", username)

.build();

DecodedJWT decodedJWT = verifier.verify(token);

System.out.println(decodedJWT.getExpiresAt());

return true;

} catch (JWTVerificationException exception) {

return false;

}

}


public static String sign(String username, String secret) {

try {

Algorithm algorithm = Algorithm.HMAC256(secret);

String token = JWT.create().withClaim("username", username)

.withExpiresAt(new Date(System.currentTimeMillis() + TOKEN_VALID_TIME))

.sign(algorithm);

return token;

} catch (JWTCreationException exception) {

return null;

}

}


public static String decodeToken(String token) {


try {

DecodedJWT jwt = JWT.decode(token);

return jwt.getClaim("username").asString();

} catch (JWTDecodeException e) {

LOGGER.error("token is error", e);

return null;

}

}

}

4、   实现一个 JWTSubjectFactory ,用来生成 Subject JWT 认证不需要会话信息,需要设置不创建会话。

package com.service.servicecombshiro.auth;


import org.apache.shiro.mgt.DefaultSubjectFactory;

import org.apache.shiro.subject.Subject;

import org.apache.shiro.subject.SubjectContext;


public class JWTSubjectFactory extends DefaultSubjectFactory {

@Override

public Subject createSubject(SubjectContext context) {

context.setSessionCreationEnabled(false); // 不创建会话

return super.createSubject(context);

}

}

5、   创建一个 JWTToken ,保存 JWT 请求的 token 信息。

package com.service.servicecombshiro.auth;


import org.apache.shiro.authc.AuthenticationToken;


public class JWTToken implements AuthenticationToken {

private String token;


public JWTToken(String token) {

this.token = token;

}

@Override

public Object getPrincipal() {

return token;

}


@Override

public Object getCredentials() {

return token;

}

}

6、   实现一个 JWTRealm ,直接继承 IniRealm ,这样就可以直接使用配置文件来配置用户信息了,非常简单。主要的就是要实现 JWT token 解码和认证。

package com.service.servicecombshiro.auth;


import org.apache.shiro.authc.AuthenticationException;

import org.apache.shiro.authc.AuthenticationInfo;

import org.apache.shiro.authc.AuthenticationToken;

import org.apache.shiro.authc.ExpiredCredentialsException;

import org.apache.shiro.authc.LockedAccountException;

import org.apache.shiro.authc.SimpleAccount;

import org.apache.shiro.authc.SimpleAuthenticationInfo;

import org.apache.shiro.authz.AuthorizationInfo;

import org.apache.shiro.realm.text.IniRealm;

import org.apache.shiro.subject.PrincipalCollection;


public class JWTRealm extends IniRealm {


public JWTRealm(String resourcePath) {

super(resourcePath);

}


@Override

public boolean supports(AuthenticationToken token) {

return token != null && token instanceof JWTToken;

}


@Override

protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

String username = JWTUtils.decodeToken(principals.toString());

USERS_LOCK.readLock().lock();

try {

return this.users.get(username);

} finally {

USERS_LOCK.readLock().unlock();

}

}


@Override

protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

JWTToken jwtToken = (JWTToken) token;

String username = JWTUtils.decodeToken(jwtToken.getCredentials().toString()); //解token,获取用户名信息

SimpleAccount account = getUser(username);

if (account != null) {

if (account.isLocked()) {

throw new LockedAccountException("Account [" + account + "] is locked.");

}

if (account.isCredentialsExpired()) {

String msg = "The credentials for account [" + account + "] are expired";

throw new ExpiredCredentialsException(msg);

}

}

// token校验,根据用户、密码和token,验证token是否有效

if (!JWTUtils.verify(username, account.getCredentials().toString(), jwtToken.getCredentials().toString())) {

throw new AuthenticationException("the token is error, please renew one!");

}

// 校验成功,返回认证完的身份信息

SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username,

jwtToken.getCredentials(), getName());

return simpleAuthenticationInfo;

}


public boolean canLogin(String username, String password) {

SimpleAccount account = getUser(username);

if (account == null) {

return false;

}

if (account.getCredentials().toString().equals(password)) {

return true;

}

return false;

}

}

7、   最后就是在 HTTPServerFilter 里面对请求做身份认证,因为是无状态的,所以不需要生成会话。

package com.service.servicecombshiro.auth;


import org.apache.servicecomb.common.rest.filter.HttpServerFilter;

import org.apache.servicecomb.core.Invocation;

import org.apache.servicecomb.foundation.vertx.http.HttpServletRequestEx;

import org.apache.servicecomb.swagger.invocation.Response;

import org.apache.shiro.SecurityUtils;

import org.apache.shiro.authc.AuthenticationException;

import org.apache.shiro.authc.UsernamePasswordToken;

import org.apache.shiro.codec.Base64;

import org.apache.shiro.mgt.DefaultSecurityManager;

import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;

import org.apache.shiro.mgt.DefaultSubjectDAO;

import org.apache.shiro.realm.Realm;

import org.apache.shiro.session.mgt.DefaultSessionManager;

import org.apache.shiro.session.mgt.SessionManager;

import org.apache.shiro.subject.Subject;


public class HttpAuthFilter implements HttpServerFilter {


private DefaultSecurityManager securityManager;

private JWTRealm realm;


public HttpAuthFilter() {

realm = new JWTRealm("classpath:shiro.ini"); //使用ini的配置方法来初始化Realm

this.securityManager = new DefaultSecurityManager(realm); //初始化SecurityManager

this.securityManager.setSubjectFactory(new JWTSubjectFactory());

DefaultSessionManager sm = new DefaultSessionManager();

// 关闭会话校验任务

sm.setSessionValidationSchedulerEnabled(false);

// 关闭会话存储,否则会报异常

((DefaultSessionStorageEvaluator) ((DefaultSubjectDAO) this.securityManager.getSubjectDAO())

.getSessionStorageEvaluator()).setSessionStorageEnabled(false);

this.securityManager.setSessionManager(sm);

}


@Override

public int getOrder() {

return -10000; // 确保这个Filter在一般的filter之前先执行

}


@Override

public Response afterReceiveRequest(Invocation invocation, HttpServletRequestEx httpServletRequestEx) {

SecurityUtils.setSecurityManager(securityManager); // 因为用到了线程上下文,只支持同步编码方式

String path = httpServletRequestEx.getPathInfo();

String userInfo = httpServletRequestEx.getHeader("Authorization");

if (userInfo == null || userInfo.isEmpty()) {

return tryLogin(httpServletRequestEx, path);

}

JWTToken token = new JWTToken(userInfo);


if (path.startsWith("/auth")) { // 只对特定的资源检测

try {

Subject user = SecurityUtils.getSubject();

user.login(token); // 登录不报异常表示成功了

} catch (AuthenticationException e) {

System.out.println("Has no right!"); // 异常表示身份认证失败

return Response.create(401, "Unauthorized", e.getMessage());

}

}

return null;

}


private Response tryLogin(HttpServletRequestEx httpServletRequestEx, String path) {

if (path.equals("/login/login")) {

// 这里只是简单的获取用户密码,使用form表单的方式来提交

String username = httpServletRequestEx.getParameter("username");

String secret = httpServletRequestEx.getParameter("password");

boolean login = realm.canLogin(username, secret);

if (!login) {

return Response.create(401, "Unauthorized",

"User/Password is not right!");

}

String token = JWTUtils.sign(username, secret);

return Response.createSuccess(token);

}

return Response.create(401, "Unauthorized",

"JWT Token is missing, please login first!");

}

}

查看下效果,首先请求登录,生成一个 JWT Token

珠联璧合 | ServiceComb 集成 Shiro 实践

再使用 token 请求下正常接口

珠联璧合 | ServiceComb 集成 Shiro 实践

如果不带上 token 或者错误 token 以及 token 失效等时,返回 401 未授权

珠联璧合 | ServiceComb 集成 Shiro 实践

上面已经实现了身份认证,有时候还需要对资源进行细粒度控制,比如有些方法只能是管理员才能调用。 Shiro 提供了三种授权方式:

  1. 编码的方式,使用硬编码的方式检查用户是否有角色或者权限,这种通常用于基于配置文件或者复杂的应用。比如角色权限都配置在配置文件或者数据库里面,需要修改后动态生效,我们可以使用自编码方式。

  2. 注解的方式,通过使用 @RequiresPermissions/@RequiresRoles ,这种方式一般都是通过 AOP 切面来实现的。

    Subject currentUser = SecurityUtils.getSubject();

    if (currentUser.hasRole("administrator")) {

    //有权限

    }

    else {

    //无权限

    }

  3. JSP 标签,现在基本上废弃了。

ServiceComb HttpServerFilter 可以直接获取到调用方法的 Method 对象,所以在 HttpServerFilter 里面可以直接使用注解的方式来进行权限角色认证,如果是遗留应用改造先前用的是注解的方式,这样就可以直接兼容,不需要再重新设计。

1、   定义 shiro 的用户角色配置文件 src/main/resources/shiro.ini ,配置文件 users 表示用户,比如 admin=123456,   administrator, viewer 表示 admin 用户,密码是 123456 ,具有 administrator,   viewer 两个角色,详细的 shiro 配置可以参考官网 https://shiro.apache.org/configuration.html

[users]

admin=123456, administrator, viewer

user1=Test123456, viewer


[roles]

administrator = *

viewer = *:get

2、   在要控制权限的方法上打上注解。

package com.service.servicecombshiro.controller;


import javax.ws.rs.core.MediaType;


import org.apache.servicecomb.provider.rest.common.RestSchema;

import org.apache.shiro.authz.annotation.RequiresRoles;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;

import org.springframework.web.bind.annotation.RequestParam;


@RestSchema(schemaId = "auth")

@RequestMapping(path = "/auth", produces = MediaType.APPLICATION_JSON)

public class ServicecombshiroImpl {


@Autowired

private ServicecombshiroDelegate userServicecombshiroDelegate;



@RequestMapping(value = "/helloworld",

produces = {"application/json"},

method = RequestMethod.GET)

@RequiresRoles(value = {"viewer"})

public String helloworld(@RequestParam(value = "name", required = true) String name) {

return userServicecombshiroDelegate.helloworld(name);

}


@RequestMapping(value = "/helloworld/admin",

produces = {"application/json"},

method = RequestMethod.POST)

@RequiresRoles("administrator")

public String admin(@RequestParam(value = "name", required = true) String name) {


return "admin " + userServicecombshiroDelegate.helloworld(name);

}

}

3、   HttpAuthFilter 里面加上角色权限校验逻辑,这里只是简单的实现,详细的实现需要覆盖所有的 shiro 的注解。

SwaggerProducerOperation swaggerProducerOperation = invocation.getOperationMeta().getExtData(Const.PRODUCER_OPERATION);

RequiresRoles requiresRoles = swaggerProducerOperation.getProducerMethod().getAnnotation(RequiresRoles.class);

if (requiresRoles != null) {

String[] roles = requiresRoles.value();

try {

user.checkRoles(roles);

} catch (AuthorizationException e) {

System.out.println("Has no required roles!"); // 异常表示权限认证失败

return Response.create(401, "Unauthorized", e.getMessage());

}

}

查看下效果,需要管理员的接口,使用 admin JWTToken 来访问,正常返回

珠联璧合 | ServiceComb 集成 Shiro 实践

使用普通用户的 JWTToken 来访问管理员的接口,返回没有权限:

珠联璧合 | ServiceComb 集成 Shiro 实践

使用普通用户的 JWTToken 来访问查询接口,正常返回:

珠联璧合 | ServiceComb 集成 Shiro 实践

Apache Shiro 是一款功能强大的安全框架, ServiceComb 集成使用相对来说也比较简单,通过这个简单的实践,能让 ServiceComb 用户知道怎样集成 Shiro 和大概的实现原理,也希望后续作为一个子项目,直接支持 Shiro 集成,方便用户使用。

项目托管地址: https://github.com/servicestage-demo/servicecomb-shiro-samples

珠联璧合 | ServiceComb 集成 Shiro 实践

珠联璧合 | ServiceComb 集成 Shiro 实践

有趣的灵魂在等你

珠联璧合 | ServiceComb 集成 Shiro 实践

长按二维码识别

文章好看就点这里

珠联璧合 | ServiceComb 集成 Shiro 实践

珠联璧合 | ServiceComb 集成 Shiro 实践

戳原文,更多精彩内容!

原文  http://mp.weixin.qq.com/s?__biz=MzUxNTEwNTg5Mg==&mid=2247488603&idx=1&sn=b8294b9ed94fe4967a04bab4fbc67489
正文到此结束
Loading...