Casdoor를 위한 OIDC 통합을 가진 Spring Security 필터
Casdoor는 OIDC 및 다양한 다른 프로토콜을 지원하는 오픈 소스 IDP입니다. 이 글에서는 Spring Security 필터와 OIDC를 사용하여 애플리케이션과 Casdoor를 통합하는 방법을 살펴봅니다.
1단계: Casdoor 배포
먼저, Casdoor 서버를 배포해야 합니다. 서버 설치 지침에 대해서는 공식 문서를 참조하십시오. 성공적인 배포 후, 확인하십시오:
- Casdoor 서버가 http://localhost:8000에서 실행 중입니다.
- Casdoor 로그인 페이지를 http://localhost:7001에서 볼 수 있습니다.
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
를 정의합니다. 사용자가 로그인한 후, 서버로 리디렉트되며code
와state
를 가지게 됩니다. 그런 다음 서버는 이 두 매개변수를 통해 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의 로그인 페이지로 리디렉트됩니다.
로그인한 후에는 /
로 리디렉트됩니다.