Tagbangers Blog

Spring Security で認証・認可情報をユーザの更新時に再読込みする方法

Spring Security でログイン認証を行う場合、認証情報と認可情報はセッションに保存される。
※認証情報=ログインが成功したか
※認可情報=どのような権限があるか

つまり、一度ログインすれば、ログアウトするまでログインした時の認証と認可情報が継続する。
この場合、困るのは管理者が対象ユーザをユーザ管理機能などで、権限を変更したり、削除した場合だ。
基本的には管理者が操作したそのタイミングでその時ログインしているユーザにも内容を反映したい。

これに対応するには2つのアプローチ方法が考えられる。

  1. ユーザのアクセスのたびに認証・認可情報を再読込みする
  2. 管理者がユーザを更新した時にセッション情報を更新する

1 はページ遷移するたびにデータソースにアクセスする必要があるため、パフォーマンスは悪い。発生頻度によるが、2 の方がそれが必要なときだけ実行されるので効率は良い。
ただし、2 は冗長構成においてセッションレプリケーションが必須となる。
今回は 1 による対応方法を記載する。

ユーザのアクセスのたびに認証・認可情報を再読込みする

Spring Security では Authentication インターフェイスが認証・認可情報を表している。
Authentication は SecurityContext にセットされ、SecurityContextRepository によって保存される。
通常は、SecurityContextRepository の実装として、HttpSessionSecurityContextRepository が利用され、SecurityContext は セッションに保存される。

今回は、HttpSessionSecurityContextRepository を利用せず、SecurityContextRepository の別の実装を用意して対応する。
以下がそのサンプル。

@Component
public class AuthorizedUserIdSecurityContextRepository implements SecurityContextRepository {

    public static final String AUTHORIZED_USER_ID_KEY = "AUTHORIZED_USER_ID";
    protected final Log logger = LogFactory.getLog(this.getClass());

    @Inject
    private UserService userService;

    public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
        SecurityContext context = SecurityContextHolder.createEmptyContext();

        HttpServletRequest request = requestResponseHolder.getRequest();
        HttpSession httpSession = request.getSession(false);

        Long authorizedUserId = readAuthorizedUserIdFromSession(httpSession);

        if (authorizedUserId != null) {
            User user = userService.readUserById(authorizedUserId);
            if (user != null) {
                AuthorizedUser authorizedUser = new AuthorizedUser(user);
                context.setAuthentication(new UsernamePasswordAuthenticationToken(authorizedUser, null, authorizedUser.getAuthorities()));
            }
        }
        return context;
    }

    public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
        Authentication authentication = context.getAuthentication();
        if (authentication == null) {
            HttpSession httpSession = request.getSession(false);
            if (httpSession != null) {
                httpSession.removeAttribute(AUTHORIZED_USER_ID_KEY);
            }
        } else if (authentication.getPrincipal() != null && authentication.getPrincipal() instanceof AuthorizedUser) {
            HttpSession httpSession = request.getSession(!response.isCommitted());
            if (httpSession != null) {
                AuthorizedUser authorizedUser = (AuthorizedUser) authentication.getPrincipal();
                httpSession.setAttribute(AUTHORIZED_USER_ID_KEY, authorizedUser != null ? authorizedUser.getId() : null);
            }
        }
    }

    public boolean containsContext(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session == null) {
            return false;
        }
        return session.getAttribute(AUTHORIZED_USER_ID_KEY) != null;
    }


    private Long readAuthorizedUserIdFromSession(HttpSession httpSession) {
        if (httpSession == null) {
            return null;
        }

        Object authorizedUserIdFromSession = httpSession.getAttribute(AUTHORIZED_USER_ID_KEY);
        if (authorizedUserIdFromSession == null) {
            return null;
        }

        if (!(authorizedUserIdFromSession instanceof Long)) {
            return null;
        }
        return (Long) authorizedUserIdFromSession;
    }
}

上記の AuthorizedUserIdSecurityContextRepository ではセッションに対象となるユーザの ID のみを保存する。
Authentication を読み込む際は Service クラスを通してセッションに保存されている ID から最新のユーザ情報を取得し、都度生成する。
※Service クラスでデータベースにアクセスする想定

次に設定方法を記載する。(Java Based Configuration)

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Inject
    private AuthorizedUserIdSecurityContextRepository authorizedUserIdSecurityContextRepository;
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.setSharedObject(SecurityContextRepository.class, authorizedUserIdSecurityContextRepository);
        // @formatter:off
        http
            .authorizeRequests()
                .anyRequest().authenticated().and()
            .formLogin();
        // @formatter:on
    }
    ...skip
} <br>

これで、セッションにはログインユーザの ID のみが保存され、アクセスのたびに認証・認可情報がデータベースから読み込まれる。