目的
tokenを使ってREST APIにアクセスし、その認証・認可をApache Shiro で行いたい。
そして、1.4 からJAX-RSとの連携が強化されているようなので、それを試してみたい。
環境
Apache Shiro 1.4.0
Jersey 2.25.1
build.gradle のdependencies に指定
compile group: 'org.apache.shiro', name: 'shiro-web', version: '1.4.0'
compile group: 'org.apache.shiro', name: 'shiro-jaxrs', version: '1.4.0'
compile group: 'org.glassfish.jersey.core', name: 'jersey-server', version: '2.25.1'
compile group: 'org.glassfish.jersey.containers', name: 'jersey-container-servlet', version: '2.25.1'
認証・認可の処理を書く
作成するクラスは以下の4つ
- MyAuthenticationToken
- MyRolePermissoionResolver
- MyRealm
- MyFilter
MyAuthenticationTokenの作成
org.apache.shiro.authc.AuthenticationToken
を継承したクラスを作成
tokenからユーザー情報(userId,roles)を返すようにする。
とりあえず、紐付いているものがなければ、null を返す。
public class MyAuthenticationToken implements AuthenticationToken{
private String token;
public MyAuthenticationToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
User user = UserManager.getUser(token);
return user;
}
@Override
public Object getCredentials() {
return token;
}
}
MyRolePermissoionResolverの作成
org.apache.shiro.authz.permission.RolePermissionResolver
を継承したクラス
role から permissionの情報を返すようにする
public class MyRolePermissionResolver implements RolePermissionResolver{
@Override
public Collection<Permission> resolvePermissionsInRole(String roleString) {
return RoleManager.getPermissions(roleString);
}
}
MyRealm の作成
org.apache.shiro.realm.AuthorizingRealm
を継承したクラス
認証と認可の処理を書く。
あと、MyAuthenticationTokenをサポートしていることを示すために、support メソッドをオバーライドする。
public class MyRealm extends AuthorizingRealm{
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
User u = (User)principals.getPrimaryPrincipal();
AuthorizationInfo info = new SimpleAuthorizationInfo(u.getRoles() );
return info;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
MyAuthenticationToken utoken = (MyAuthenticationToken)token;
User user = (User)token.getPrincipal();
if(user == null){
throw new AuthenticationException("Invalid token: " + String.valueOf(token.getCredentials()));
}
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, utoken.getCredentials(), this.getClass().getName());
return info;
}
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof MyAuthenticationToken;
}
}
MyFilter の作成
javax.servlet.Filter
を継承したクラス
doFilter 内で、リクエストからtokenを取得し、Shiroに対して認証処理を行う
public class MyFilter implements Filter{
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest)request;
String apitoken = req.getHeader("token");
Subject currentUser = SecurityUtils.getSubject();
AuthenticationToken token = new MyAuthenticationToken(apitoken);
try{
currentUser.login(token);
}catch(AuthenticationException e){
HttpServletResponse res = (HttpServletResponse)response;
res.setContentType("application/json");
res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
try{
chain.doFilter(req, response);
}finally{
// 一度の呼び出しでセッション情報を破棄する
currentUser.getSession().stop();
}
}
}
Shiro の設定
上で作成したクラスをWEB-INF/shiro.ini
に設定する
設定する項目は以下
- securityManager.realms : MyRealmを設定
- securityManager.authorizer.rolePermissionResolver : MyRolePermissoionResolver
- filters : urlsに指定できるように、MyFilterの定義をする
- urls : 認証・認可をかけたいurlと定義したFilterを設定する
[main]
cacheManager = org.apache.shiro.cache.MemoryConstrainedCacheManager
securityManager.cacheManager = $cacheManager
myRealm = sample.shiro.MyRealm
securityManager.realms = $myRealm
globalRolePermissionResolver = sample.shiro.MyRolePermissionResolver
securityManager.authorizer.rolePermissionResolver = $globalRolePermissionResolver
[filters]
myFilter=sample.shiro.MyFilter
[urls]
/** = myFilter
REST側の実装
仕様
user一覧取得
とuser作成
を実装する。
* user一覧
は、admin
,user
role を保持しているユーザーが取得可能であり、admin
role 保持者の場合、token情報を含んだデータを取得できる。
* user作成
は、user:create
permission が必要である。
実装
- ShiroFeature を利用し、認証認可の設定は、アノテーション(RequiresRoles, RequiresPermissions)で指定
UserResource.java
@Path("/users")
public class UserResource {
@GET
@RequiresRoles(value={"user","admin"}, logical=Logical.OR)
@Produces(MediaType.APPLICATION_JSON)
public String userList(){
Collection<User> users = UserManager.list();
Gson g = new Gson();
Subject currentUser = SecurityUtils.getSubject();
if (!currentUser.hasRole("admin")){
Collection<User> notokenUser = new ArrayList<>();
for (User u : users){
User us = new User(u.getUsername(), null);
us.addRole(u.getRoles().toArray(new String[0]));
notokenUser.add(us);
}
return g.toJson(notokenUser);
}else{
return g.toJson(users);
}
}
@POST
@RequiresPermissions("user:create")
@Produces(MediaType.APPLICATION_JSON)
public String createUser(@QueryParam("username") String username, @QueryParam("roles") List<String> roles){
User u = new User(username);
for (String role: roles){
u.addRole(role);
}
UserManager.addUser(u);
return new Gson().toJson(u);
}
}
Application.java
public class Application extends javax.ws.rs.core.Application {
@Override
public Set<Class<?>> getClasses(){
Set<Class<?>> classes = new HashSet<>();
classes.add(ShiroFeature.class);
classes.add(UserResource.class);
return classes;
}
}
web.xml の設定
web.xml にShiro と Jeseryの設定を記述
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<listener>
<listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>RestServer</servlet-name>
<servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
<init-param>
<param-name>javax.ws.rs.Application</param-name>
<param-value>sample.jersey.Application</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>RestServer</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
<filter>
<filter-name>ShiroFilter</filter-name>
<filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>ShiroFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>FORWARD</dispatcher>
<dispatcher>INCLUDE</dispatcher>
<dispatcher>ERROR</dispatcher>
</filter-mapping>
</web-app>
sample 実行
動作確認用のコードは以下です。
サーバーの起動
gradle appRun
以降、curl を使って動作を確認します。
- tokenなしでアクセスすると401
$ curl -v http://localhost:8080/sample-shiro/users
* Hostname was NOT found in DNS cache
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /sample-shiro/users HTTP/1.1
> User-Agent: curl/7.35.0
> Host: localhost:8080
> Accept: */*
>
< HTTP/1.1 401 Unauthorized
< Date: Sat, 01 Jul 2017 03:02:35 GMT
< Set-Cookie: rememberMe=deleteMe; Path=/sample-shiro; Max-Age=0; Expires=Fri, 30-Jun-2017 03:02:35 GMT
< Content-Type: application/json
< Content-Length: 0
* Server Jetty(9.2.22.v20170606) is not blacklisted
< Server: Jetty(9.2.22.v20170606)
<
* Connection #0 to host localhost left intact
- adminロールでアクセスするとtokenつきで取得
$ curl -H "token: admintoken" -H "Accept: application/json" -H "Content-type: application/json" \
http://localhost:8080/sample-shiro/users
[{"username":"user1","token":"usertoken","roleSet":["user"]},{"username":"admin","token":"admintoken","roleSet":["admin","user"]}]
- userロールだとtokenなしで取得
$ curl -H "token: usertoken" -H "Accept: application/json" -H "Content-type: application/json" \
http://localhost:8080/sample-shiro/users
[{"username":"user1","roleSet":["user"]},{"username":"admin","roleSet":["admin","user"]}]
-
user:create
permissionを持っているadmin
ロールで、ユーザーの作成
$ curl -H "token: admintoken" -H "Accept: application/json" -H "Content-type: application/json" \
-X POST "http://localhost:8080/sample-shiro/users?username=test&role=admin&role=user"
{"username":"test","token":"857b6a31-09bd-4316-9922-f87fcadf8541","roleSet":["admin","user"]}
-
user:create
permissionを持っていないuserロールで、ユーザーの作成
$ curl -H "token: usertoken" -H "Accept: application/json" -H "Content-type: application/json" \
-X POST "http://localhost:8080/sample-shiro/users?username=test&role=admin&role=user"
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>Error 403 Forbidden</title>
</head>
<body><h2>HTTP ERROR 403</h2>
<p>Problem accessing /sample-shiro/users. Reason:
<pre> Forbidden</pre></p><hr><i><small>Powered by Jetty://</small></i><hr/>
</body>
</html>
error がHTMLででてしまったが、多分、ExceptionMapper を設定すればいけると思う。。。