Casdoor用のOIDC統合を持つSpring Securityフィルター
Casdoorは、OIDCおよびその他のさまざまなプロトコルをサポートするオープンソースのIDPです。 この記事では、Spring SecurityフィルターとOIDCを使用してアプリケーションにCasdoorを統合する方法について見ていきます。
ステップ1: Casdoorをデプロイする
まず、Casdoorサーバーをデプロイする必要があります。 サーバーのインストール手順については、公式ドキュメントを参照してください。 デプロイに成功したら、以下を確認してください:
- Casdoorサーバーがhttp://localhost:8000で稼働している。
- http://localhost:7001でCasdoorのログインページが表示される。
- ログイン機能をテストするには、
admin
と123
の資格情報でログインしてみてください。
これらのステップを確認した後、以下のステップに従ってアプリケーションにCasdoorを統合してください。
ステップ2: Casdoorアプリケーションを設定する
- 新しいCasdoorアプリケーションを作成するか、既存のものを使用します。
- リダイレクトURLを追加します。 リダイレクトURLの取得方法については、次のセクションで詳しく説明します。
- 証明書編集ページで
Certificate
を取得します。 - 必要に応じてプロバイダーその他の設定を追加します。
Application Name
、Organization Name
、Redirect URL
、Client ID
、Client Secret
、Certificate
の値は、アプリケーション設定ページで取得できます。 次のステップでこれらを使用します。
ステップ3: Spring Securityを設定する
トークンを処理するためのSpring Securityフィルターの設定をカスタマイズできます:
特に<Client ID>
など、Casdoorインスタンスの設定値を自分のものに置き換えてください。
server:
port: 8080
casdoor:
endpoint: http://CASDOOR_HOSTNAME:8000
client-id: <Client ID>
client-secret: <Client Secret>
certificate: <Certificate>
organization-name: <Organization Name>
application-name: <Application Name>
redirect-url: http://FRONTEND_HOSTNAME/callback
フロントエンドアプリケーションの場合、<FRONTEND_HOSTNAME>
のデフォルト値はlocalhost:3000
です。 このデモでは、リダイレクトURLはhttp://localhost:3000/callback
です。 これをcasdoor
アプリケーションで設定してください。
ステップ4: フロントエンドを設定する
casdoor-js-sdk
をインストールし、以下のようにSDKを設定する必要があります:
casdoor-js-sdk
をインストールします。npm i casdoor-js-sdk
# or
yarn add casdoor-js-sdkSDK
を設定します。import Sdk from "casdoor-js-sdk";
// Serverurl is the URL where spring security is deployed
export const ServerUrl = "http://BACKEND_HOSTNAME:8080";
const sdkConfig = {
serverUrl: "http://CASDOOR_HOSTNAME:8000",
clientId: "<your client id>",
appName: "<your application name>",
organizationName: "<your organization name>",
redirectPath: "/callback",
};
export const CasdoorSDK = new Sdk(sdkConfig);
ステップ5: デモをセットアップする
Spring Bootアプリケーションを作成します。
JWTを処理するためのいくつかの設定を追加します。
@EnableWebSecurity
public class SecurityConfig {
private final JwtTokenFilter jwtTokenFilter;
public SecurityConfig(JwtTokenFilter jwtTokenFilter) {
this.jwtTokenFilter = jwtTokenFilter;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// enable CORS and disable CSRF
http = http.cors(corsConfig -> corsConfig
.configurationSource(configurationSource())
).csrf().disable();
// set session management to stateless
http = http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and();
// set permissions on endpoints
http.authorizeHttpRequests(authorize -> authorize
.mvcMatchers("/api/redirect-url", "/api/signin").permitAll()
.mvcMatchers("/api/**").authenticated()
);
// set unauthorized requests exception handler
http = http
.exceptionHandling()
.authenticationEntryPoint(
(request, response, ex) -> ResponseUtils.fail(response, "unauthorized")
)
.and();
// add JWT token filter
http.addFilterBefore(
jwtTokenFilter,
UsernamePasswordAuthenticationFilter.class
);
return http.build();
}
// ...
}
```
トークン検証が必要なリクエストをインターセプトするシンプルなJWTフィルターを定義します。
@Component
public class JwtTokenFilter extends OncePerRequestFilter {
private final CasdoorAuthService casdoorAuthService;
public JwtTokenFilter(CasdoorAuthService casdoorAuthService) {
this.casdoorAuthService = casdoorAuthService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain)
throws ServletException, IOException {
// get authorization header and validate
final String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if (!StringUtils.hasText(header) || !header.startsWith("Bearer ")) {
chain.doFilter(request, response);
return;
}
// get jwt token and validate
final String token = header.split(" ")[1].trim();
// get user identity and set it on the spring security context
UserDetails userDetails = null;
try {
CasdoorUser casdoorUser = casdoorAuthService.parseJwtToken(token);
userDetails = new CustomUserDetails(casdoorUser);
} catch (CasdoorAuthException exception) {
logger.error("casdoor auth exception", exception);
chain.doFilter(request, response);
return;
}
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails,
null,
AuthorityUtils.createAuthorityList("ROLE_casdoor")
);
authentication.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
}
}認証が必要なインターフェースにユーザーがアクセスすると、
JwtTokenFilter
はリクエストヘッダーAuthorization
からトークンを取得し、それを検証します。Casdoorにログインするユーザーを処理する
Controller
を定義します。 ユーザーがCasdoorにログインすると、サーバーにリダイレクトされ、code
とstate
を持ってきます。 サーバーは、これら2つのパラメータを使用してCasdoorからユーザーの身元を確認し、token
を取得する必要があります。@RestController
public class UserController {
private static final Logger logger = LoggerFactory.getLogger(UserController.class);
private final CasdoorAuthService casdoorAuthService;
// ...
@PostMapping("/api/signin")
public Result signin(@RequestParam("code") String code, @RequestParam("state") String state) {
try {
String token = casdoorAuthService.getOAuthToken(code, state);
return Result.success(token);
} catch (CasdoorAuthException exception) {
logger.error("casdoor auth exception", exception);
return Result.failure(exception.getMessage());
}
}
// ...
}
```
ステップ6: デモを試す
ブラウザを通じてフロントエンドアプリケーションにアクセスできます。 ログインしていない場合は、ログインボタンが表示されます。 それをクリックすると、Casdoorのログインページにリダイレクトされます。
ルートページにアクセスすると、
Casdoor Login
ボタンをクリックすると、ページはCasdoorのログインページにリダイレクトされます。
ログイン後、/
にリダイレクトされます。