Apache Shiro で自作のRealm を作って認証・認可を行う

## 目的

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'
```

## 認証・認可の処理を書く

作成するクラスは以下の４つ

* MyAuthenticationToken
* MyRolePermissoionResolver
* MyRealm
* MyFilter

### MyAuthenticationTokenの作成

`org.apache.shiro.authc.AuthenticationToken`を継承したクラスを作成

tokenからユーザー情報(userId,roles)を返すようにする。
とりあえず、紐付いているものがなければ、null を返す。

```java
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の情報を返すようにする

```java
public class MyRolePermissionResolver implements RolePermissionResolver{

	
	@Override
	public Collection<Permission> resolvePermissionsInRole(String roleString) {
		return RoleManager.getPermissions(roleString);
	}

}

```

### MyRealm の作成

`org.apache.shiro.realm.AuthorizingRealm`を継承したクラス

認証と認可の処理を書く。
あと、MyAuthenticationTokenをサポートしていることを示すために、support メソッドをオバーライドする。

```java
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に対して認証処理を行う

```java
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

```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

```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
<?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 実行

動作確認用のコードは以下です。

https://github.com/ihgs/sample-shiro


サーバーの起動

```bash
gradle appRun
```

以降、curl を使って動作を確認します。


* tokenなしでアクセスすると401

```bash
$ 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つきで取得

```bash
$ 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なしで取得

```bash
$ 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`ロールで、ユーザーの作成

```bash
$ 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ロールで、ユーザーの作成

```bash
$ 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 を設定すればいけると思う。。。