ServiceComb integrated Shiro practice | Naruto special release

Shiro 简介

Apache Shiro is a powerful, easy to use lightweight open source Java security framework, which mainly provides authentication, authorization, encryption and session management. Spring Security is probably the most extensive security framework with the industry, but the Spring Security and Spring coupling too, from the Spring framework will not use, so a lightweight security framework is sometimes a very good choice.

Shiro API to provide security mainly through the use of four:

  • Authentication Authentication - provides user identity can be understood as login authentication.
  • Authorization Authorization - access control, which is commonly spoken ACL (Access Control List) of RBAC (Role Base Access Control) or ABAC (Attribute Base Access Control).
  • Encryption Cryptography - encryption to protect data and ensure data security.
  • Session Management Session Management - after login session management, Shiro has a separate session management mechanism, can be J2EE session, it can also be a normal Java applications.

Shiro has several key core concepts: Subject, SecurityManager and Realms, we simply introduce these concepts under the meaning of:

Subject
privilege main responsibility is designed to allow the system to identify the object to be managed, such as the user's system in general, this is not necessarily a person, it can be a piece of equipment, there are Subject logon, logoff, privilege detection operation. All Subject SecurityManager will be bound to the above, all Subject interactions will be entrusted to the SecurityManager.

SecurityManager
security manager, and all security-related operations will be dealing with SecurityManager, which manages all of the Subject, it is the core architecture Shiro

Realm
field, Shiro Safety Data from the Realm. Shiro Realm bridge between the actor and applications, such as user, the list of roles. Custom application may achieve different Realm, Shiro Realm also provides several out of the box, such SimpleAccountRealm, IniRealm, JdbcRealm and DefaultLdapRealm, JndiRealm. With these simple Realm we can easily get started Shiro, basically all the customized extension points are achieved Realm custom.

Since Shiro can provide such a comprehensive, easy to use security permissions function, then ServiceComb is not it also can be very easy to integrate it?

The answer of course is it.

Simple integration

ServiceComb integrated Shiro, two schemes can be used, one is integrated Vertx-shiro, using this method is provided using the Transport Rest over Vertx way, another is to use a handler or HttpServerFilter ServiceComb extension point mechanism.

The first way is to use the advantages of an asynchronous manner, full use of vertx extension mechanism, associated with ServiceComb not only needs to be extended to achieve a org.apache.servicecomb.transport.rest.vertx.VertxHttpDispatcher, init method to authenticate logic applied to filter URL.

1, the introduction vertx-shiro dependency in the POM

<dependency>
    <groupId>io.vertx</groupId> <artifactId>vertx-auth-shiro</artifactId> <version>3.6.3</version> </dependency>

2, increase user vertx-shiro, roles, profiles 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, extension implementation 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(); }); }); } }

The second way is to use the mechanism of the extension point, using the example HttpServerFilter extension point mechanism, all the requests will come HttpServerFilter REST logic. Specific achieve the following:

1, rely on the introduction of shiro

<dependency>
    <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.4.1</version> </dependency>

2, the definition of shiro user information file src \ main \ resources \ shiro.ini file

[users]
admin=123456
user1=Test123456

3, using the SPI mechanisms to achieve a HttpServerFilter to do authentication, this simple example we use Http Basic Auth authentication method to basic authentication. You must first initialize a SecurityManager, and injected into a Realm, then get the identity information in afterReceiveRequest method, and do check identity information. (Shiro Since many implementations of the current thread context are used to pass SecurityManager, so the synchronization code using only the present example of embodiment)

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, sends a request for authentication

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

Distributed Integration

Of micro-services system, the application usually stateless, so the server does not implement session mechanism generally conventional J2EE container, but the use of an external session, Oath2 protocol, a session may be used without the program, each request clients bring identity, the identity of the server can identify the client, this program is a typical implementation of JWT.

1, introduction and Shiro dependent JWT

<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 defined user profile src \ main \ resources \ shiro.ini

[users]
admin=123456
user1=Test123456

3, to achieve a JWTUtils, mainly used for JWT Token signature and verification

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, to achieve a JWTSubjectFactory, used to generate the Subject, JWT authentication session information is not required, you need to set a session is not created.

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, create a JWTToken, save the token information JWT request.

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, to achieve a JWTRealm, the direct successor IniRealm, so that you can directly use the configuration file to configure user information, and very simple. The main thing is to realize JWT decode the token and authentication.

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(www.jiuyueguojizc.cn ).equals(password)) { return true; } return false; } } 

7, the last is in HTTPServerFilter inside of authenticating the request, because it is stateless, so no need to generate conversation.

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) (www.shicaiyl.com (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(www.baihuajtuan.cn); 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(www.huizhonggjzc.cn"password"); boolean login = realm.canLogin(username, secret); if (!login) { return Response.create(401, "Unauthorized", "User/Password is not right!"); } String token = JWTUtils.sign(username,www.uuedzc.cn secret); return Response.createSuccess(token); } return Response.create(401, "Unauthorized", "JWT Token is missing, please login first!"); } }

See the effect, the first logon request, generates a JWT Token

Under normal token request reuse Interface

If the tape is not token or token such as the failure and the error token, return 401 Unauthorized

Authorize

The above has been achieved authentication, sometimes need fine-grained control of resources, for example, some methods can only be an administrator to call. Shiro authorization provides three ways:

 编码的方式,使用硬编码的方式检查用户是否有角色或者权限,这种通常用于基于配置文件或者复杂的应用。比如角色权限都配置在配置文件或者数据库里面,需要修改后动态生效,我们可以使用自编码方式。
注解的方式,通过使用@RequiresPermissions/@RequiresRoles,这种方式一般都是通过AOP切面来实现的。

Subject currentUser = SecurityUtils.getSubject(www.lafei6v.cn );
if (currentUser.hasRole("administrator"www.ueddpt.com)) {
//有权限
}
else {
//无权限 }

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"www.shentuylgw.cn}, method = RequestMethod.GET) @RequiresRoles(value = {"viewer"}) public String helloworld(@RequestParam(value = "name", required = true) String name) { return userServicecombshiroDelegate.helloworld(name); } @RequestMapping(value =www.wujiu5zhuce.cn "/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来访问,正常返回:

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

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

总结

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

Guess you like

Origin www.cnblogs.com/laobeipai/p/12093471.html