LoginSignup
5
6

More than 5 years have passed since last update.

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

Posted at

目的

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,userrole を保持しているユーザーが取得可能であり、admin role 保持者の場合、token情報を含んだデータを取得できる。
* user作成は、user:createpermission が必要である。

実装

  • 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:createpermissionを持っていない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 を設定すればいけると思う。。。

5
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
6