概要
Spring Securityの利用にあたり、逆引きという形でまとめました。ここに載っていることが正解ではなく、あくまでもひとつの方法ですので実装に迷ったときに参考にしていただければ幸いです。
リファレンスは随時更新していきます。
環境
項目 | バージョン |
---|---|
Java | 8 |
Spring Boot | 2.2.4 |
準備
以下は最低限必要なコードです。
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.authenticated();
}
}
今回はDBに接続しないので、以下の認証情報を利用します。
spring:
security:
user:
name: user
password: pass
roles: USER
注意事項
- コード中に(A)、(B)など出てきますが、これは同じものが入ることを意味します。スコープは各リファレンス上のみとなります。
- 画面表示用のコントローラークラス(
@Controller
)は割愛させていただきますので、適宜作成してください。
逆引き
共通編
特定のパスは制限をかけたくない
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/","/login") // 「/」「/login」は認証不要でアクセス可能
.permitAll()
.anyRequest()
.authenticated();
}
}
CSRFを無効にしたい
デフォルトでCSRFが有効になっている。
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.authenticated();
http.csrf().disable();
}
}
外部(オリジンが違うサイト)からのリクエストを受け付けたい
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.authenticated();
http.cors().configurationSource(getCorsConfigurationSource());
}
private CorsConfigurationSource getCorsConfigurationSource() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
// 全てのメソッドを許可
corsConfiguration.addAllowedMethod(CorsConfiguration.ALL);
// 全てのヘッダを許可
corsConfiguration.addAllowedHeader(CorsConfiguration.ALL);
// 全てのオリジンを許可
corsConfiguration.addAllowedOrigin(CorsConfiguration.ALL);
UrlBasedCorsConfigurationSource corsSource = new UrlBasedCorsConfigurationSource();
// パスごとに設定が可能。ここでは全てのパスに対して設定
corsSource.registerCorsConfiguration("/**", corsConfiguration);
return corsSource;
}
}
認証処理をカスタマイズしたい
AuthenticationProvider
の実装クラスを作成します。
@Configuration
public class CustomeAuthenticationProvider implements AuthenticationProvider {
// 認証処理
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = (String) authentication.getPrincipal();
String password = (String) authentication.getCredentials();
if (!"user".equals(username) || !"password".equals(password)) {
throw new BadCredentialsException("ログイン情報が間違っています");
}
return new UsernamePasswordAuthenticationToken(username, password, new ArrayList<>());
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}
SecurityConfig
に上記で作成したProviderを指定します。
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomeAuthenticationProvider authenticationProvider;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.authenticated();
http.formLogin();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 上記で作成したProviderを指定
auth.authenticationProvider(authenticationProvider);
}
}
Form認証編
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.authenticated();
http.formLogin();
}
}
/login
にアクセスすると、Spring Securityで用意されたログイン画面が表示される。
独自のログイン画面を利用したい
ログイン画面のHTMLを用意します。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>ログイン</title>
</head>
<body>
<form th:action="@{/login}" method="post">
<input type="text" name="username">
<input type="password" name="password">
<button type="submit">ログイン</button>
</form>
</body>
</html>
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("(A)") // 追加
.permitAll() // 追加
.anyRequest()
.authenticated()
http.formLogin()
.loginPage("(A)"); // 追加
}
}
認証用のパラメータを変更したい
デフォルトでは、username
とpassword
というパラメータ名で取得します。
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/(A)") // ①のパス
.permitAll()
.anyRequest()
.authenticated()
http.formLogin()
.loginPage("/(A)")
.usernameParameter("(B)") // 追加
.passwordParameter("(C)"); // 追加
}
}
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>ログイン</title>
</head>
<body>
<form th:action="@{/login}" method="post">
<input type="text" name="(B)">
<input type="password" name="(C)">
<button type="submit">ログイン</button>
</form>
</body>
</html>
認証成功時に遷移する画面を変更したい
デフォルトでは「/」に遷移する。
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.authenticated()
http.formLogin()
.defaultSuccessUrl("/home"); // 「/home」に遷移
}
}
3つ以上のパラメータで認証したい
通常、username
、password
という2つのパラメータで認証されます。
方法はいくつかありますが、ここではForm認証における方法です。
作成するもの
- ログイン画面
- 認証情報格納クラス
- 認証プロバイダ
- 認証フィルター
3つのパラメータを入力するログイン画面を用意します。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>ログイン</title>
</head>
<body>
<form th:action="@{/login}" method="post">
<input type="text" name="(A)">
<input type="text" name="username">
<input type="password" name="password">
<button type="submit">ログイン</button>
</form>
</body>
</html>
認証情報を保持するUsernamePasswordAuthenticationToken
を継承したクラスを作成します。
public class MultiParamAuthenticationToken extends UsernamePasswordAuthenticationToken {
private static final long serialVersionUID = 1L;
private Object tenant; // 追加パラメータ
public MultiParamAuthenticationToken(Object principal, Object credentials, Object tenant) {
super(principal, credentials, Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")));
this.tenant = tenant;
}
public Object getTenant() {
return this.tenant;
}
}
認証プロバイダークラスを作成します。
@Configuration
public class MultiParamAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = (String) authentication.getPrincipal();
String password = (String) authentication.getCredentials();
String tenant = null;
if (authentication instanceof MultiParamAuthenticationToken) {
tenant = (String) ((MultiParamAuthenticationToken) authentication).getTenant();
}
if (!"user".equals(username) || !"pass".equals(password) || !"multi".equals(tenant)) {
throw new BadCredentialsException("aaa");
}
return new MultiParamAuthenticationToken(username, password, tenant);
}
@Override
public boolean supports(Class<?> authentication) {
return MultiParamAuthenticationToken.class.isAssignableFrom(authentication);
}
}
3つ目のパラメータを取得するためのフィルタークラスを用意します。
public class MultiParamAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
String username = obtainUsername(request);
String password = obtainPassword(request);
String tenant = obtainTenant(request);
MultiParamAuthenticationToken authRequest = new MultiParamAuthenticationToken(
username, password, tenant);
return getAuthenticationManager().authenticate(authRequest);
}
private String obtainTenant(HttpServletRequest request) {
return (String) request.getParameter("tenant");
}
}
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MultiParamAuthenticationProvider authenticationProvider;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login")
.permitAll()
.anyRequest()
.authenticated();
// formLoginを利用せずに独自フィルターを設定
http.addFilter(getMultiParamAuthenticationFilter());
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider);
}
private MultiParamAuthenticationFilter getMultiParamAuthenticationFilter() throws Exception {
MultiParamAuthenticationFilter filter = new MultiParamAuthenticationFilter();
filter.setAuthenticationManager(authenticationManager());
return filter;
}
}
OAuth認証編
OAuth認証を行うにあたり、認証プロバイダでクライアントIDとシークレットを発行しておく必要があります。
ここではGoogleでOAuth認証を行います。
最低限必要な実装
pom.xmlに以下の依存関係を追加します。
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
application.ymlにOAuth情報を設定します。
spring:
security:
oauth2:
client:
registration:
google:
clientId: <クライアントID>
clientSecret: <クライアントシークレット>
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.authenticated();
http.oauth2Login(); // 追加
}
認証時の情報を利用したい
@Controller
public class LoginController {
@Autowired
private OAuth2AuthorizedClientService authorizedClientService;
@GetMapping
public String index(OAuth2AuthenticationToken authentication, Model model) {
OAuth2AuthorizedClient authorizedClient = this.authorizedClientService.loadAuthorizedClient(
authentication.getAuthorizedClientRegistrationId(),
authentication.getName());
model.addAttribute("name", authorizedClient.getPrincipalName());
model.addAttribute("accessToken", authorizedClient.getAccessToken().getTokenValue());
return "index";
}
OAuth2AuthenticationToken
で認証情報が取得できます。
次に、表示用の画面を作成します。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>ホーム</title>
</head>
<body>
<p th:text="${accessToken}"></p>
<p th:text="${name}"></p>
</body>
</html>
独自のログイン画面でOAuth認証したい
ログイン画面を作成します。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>ログイン</title>
</head>
<body>
<a href="/oauth2/authorization/google">Google認証</a>
</body>
</html>
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("(A)")
.permitAll()
.anyRequest()
.authenticated();
http.oauth2Login().loginPage("(A)"); // 追加
}
独自認証編
JSONによる認証を行いたい
UsernamePasswordAuthenticationFilter
を継承したクラスを作成します。
public class JsonAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
try {
// パラメータを取得
// 認証パラメータ情報がrequest.getInputStream()に格納されているので、Jacksonを利用して取り出している
Map<String, String> params = new ObjectMapper().readValue(request.getInputStream(),
new TypeReference<Map<String, String>>() {});
// 認証リクエスト情報生成
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
params.get("(A)"), params.get("(B)"));
// 認証
return getAuthenticationManager().authenticate(authRequest);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// 認証成功時に処理を行う
@Override
protected void successfulAuthentication(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain,
Authentication auth) throws IOException, ServletException {
// 認証済みユーザを保存
// 保存しないと、未ログイン扱いになる
SecurityContextHolder.getContext().setAuthentication(auth);
}
}
ログイン画面はAjax通信で認証情報を送信するようにします。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>ログイン</title>
</head>
<body>
<form>
<input type="text" id="(A)" name="(A)">
<input type="password" id="(B)" name="(B)">
<button type="button" id="btnLogin" onclick="login()">ログイン</button>
</form>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
function login() {
// 認証パラメータを取得
const data = {
email: document.getElementById('(A)').value,
password: document.getElementById('(B)').value
}
// 認証
axios.post('(C)', data)
.then(res => location.href = "/home") // 認証成功時は「/home」に遷移する
}
</script>
</body>
</html>
上記で作成したFilterをSecuritConfigで設定します。
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("(C)")
.permitAll()
.anyRequest()
.authenticated();
// Ajax通信を行うので無効にしておく
http.csrf().disable();
// Jsonによる認証フィルターを設定
http.addFilter(getJsonAuthenticationFilter());
}
private JsonAuthenticationFilter getJsonAuthenticationFilter() throws Exception {
JsonAuthenticationFilter filter = new JsonAuthenticationFilter();
// 認証方法はデフォルトを利用
filter.setAuthenticationManager(authenticationManager());
// Filterが実行されるパスとHTTPメソッドを指定
filter.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("(C)", "POST"));
return filter;
}
}
応用編
認証後JWTを返却したい
pom.xmlにJWTのライブラリを追加します。
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
認証情報をJSONで送信し、成功後JWTを返却します。
UsernamePasswordAuthenticationFilter
を継承したクラスを作成します。
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
try {
// パラメータを取得
// 認証パラメータ情報がrequest.getInputStream()に格納されているので、Jacksonを利用して取り出している
Map<String, String> params = new ObjectMapper().readValue(request.getInputStream(),
new TypeReference<Map<String, String>>() {});
// 認証リクエスト情報生成
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
params.get("(A)"), params.get("(B)"));
// 認証
return getAuthenticationManager().authenticate(authRequest);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// 認証成功時にJWTの作成を行い、レスポンスヘッダに設定する。
@Override
protected void successfulAuthentication(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain,
Authentication auth) throws IOException, ServletException {
Date issuedAt = new Date();
Date notBefore = new Date(issuedAt.getTime());
Date expiresAt = new Date(issuedAt.getTime() + TimeUnit.MINUTES.toMillis(100L));
// JWT生成
String token = JWT.create()
.withIssuedAt(issuedAt)
.withNotBefore(notBefore)
.withExpiresAt(expiresAt)
.withClaim("(C)", (String)auth.getPrincipal())
.sign(Algorithm.HMAC512("secret"));
// AuthorizationヘッダにJWTを設定
res.addHeader("Authorization", "Bearer " + token);
}
}
OncePerRequestFilter
を継承したJWTを検証するクラスを作成します。
public class JwtAuthorizationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
throws ServletException, IOException {
String header = req.getHeader("Authorization");
if (header == null || !header.startsWith("Bearer ")) {
// 処理を続行しますが、SecurityContextに認証情報を設定しないので、結果的に403が返却されます
chain.doFilter(req, res);
return;
}
// AuthorizationヘッダのBearer Prefixである場合
UsernamePasswordAuthenticationToken authentication = getAuthentication(req);
if (Objects.isNull(authentication)) {
chain.doFilter(req, res);
return;
}
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(req, res);
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
String token = request.getHeader("Authorization");
if (token == null || !token.startsWith("Bearer ")) {
return null;
}
token = token.substring("Bearer ".length());
if (Objects.isNull(token) || token.length() == 0) {
return null;
}
JWTVerifier verifier = JWT.require(Algorithm.HMAC512("secret")).build();
try {
DecodedJWT jwt = verifier.verify(token);
return new UsernamePasswordAuthenticationToken(
jwt.getClaim("(C)").asString(), null,
null);
} catch (JWTDecodeException e) {
return null;
}
}
}
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login")
.permitAll()
.anyRequest()
.authenticated();
http.csrf().disable();
http.addFilter(getJsonAuthenticationFilter());
}
private JsonAuthenticationFilter getJsonAuthenticationFilter() throws Exception {
JsonAuthenticationFilter filter = new JsonAuthenticationFilter();
// 認証方法はデフォルトを利用
filter.setAuthenticationManager(authenticationManager());
// Filterが実行されるパスとHTTPメソッドを指定
filter.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login", "POST"));
return filter;
}
}