R-SQUARE.net

研究開発部

UserDetailServiceでカスタムPrincipalをつくる

  • SpringFrameworkの兄弟分SpringSecurityは、認証認可をサクッと実装できちゃうフレームワーク。
  • 認証に成功するとusername(ユーザIDでもログインIDでもなんでもいいや)やauthority情報を持つprincipalオブジェクトが作られる。
  • principalオブジェクトにはHttpServerRequestオブジェクト(request.getUserPrincipal())でアクセスできる。
  • またSpringSecurityのUserDetailsを実装したクラスを作ると認証情報に付帯するユーザ情報(メアドとか所属とか住所とか)を載せられる。

ソースコード抜粋(build.gradle)

gradle依存関係に、SpringSecurityのために必要なライブラリを設定。昨今のWebアプリではユーザ情報はDBにしまっとくのが定石だからhibernateやらmysqlやらのライブラリを設定しなくちゃだよ。ログイン画面もデフォルトのはイケてないから、オリジナルで作るためにビューエンジン関係のライブラリもご紹介。thymeleaf-extras-springsecurity4はthymeleafビューのhtmlの中でSpringSecurityの認証関係オブジェクトにアクセスするための便利ライブラリ。
dependencies {
  //core
  compile "org.springframework:spring-webmvc:4.2.1.RELEASE"
  compile "org.springframework.security:spring-security-web:4.0.2.RELEASE"
  compile "org.springframework.security:spring-security-config:4.0.2.RELEASE"
  
  //view
  compile "org.thymeleaf:thymeleaf-spring4:2.1.4.RELEASE"
  compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity4:2.1.2.RELEASE'
  
  //data
  compile "org.springframework:spring-orm:4.2.1.RELEASE"
  compile "org.hibernate:hibernate-core:5.0.1.Final"
  compile "org.apache.commons:commons-dbcp2:2.1.1"
  compile "mysql:mysql-connector-java:5.1.36"
  compile 'org.hibernate:hibernate-validator:5.2.1.Final'

  //他にもたくさん.....
}


ソースコード抜粋(Java Config)

package net.rsquare.r2bingo.web.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

@Configuration
@ComponentScan(basePackages = { "net.rsquare.*" })
@EnableWebSecurity
@PropertySource(value = { "classpath:app.properties" })
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	@Value("${map.handler.js}") private String handlerJs;
	@Value("${map.handler.css}") private String handlerCss;
	@Value("${map.handler.images}") private String handlerImg;
	
	@Autowired
	@Qualifier("userDetailsService")
	UserDetailsService userDetailsService;
	
	@Autowired
	public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
//		auth.inMemoryAuthentication()
//			.withUser("hoge").password("moga").authorities("user")
//		.and()
//			.withUser("admin").password("admin").authorities("admin","user");

		auth.userDetailsService(userDetailsService);
	}

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.authorizeRequests()
				.antMatchers("/demo/**").permitAll()
				.antMatchers("/events/**/demo","/drawing/**/demo").permitAll()
				.antMatchers("/lottery/**").permitAll()
				.antMatchers(handlerJs,handlerCss,handlerImg).permitAll()
				.antMatchers("/events/**","/drawing/**").hasAuthority("user")
				.antMatchers("/hello/**").hasAuthority("user")
				.antMatchers("/mypage/**").hasAuthority("user")
				.antMatchers("/manage/**").hasAuthority("admin")
				.antMatchers("/**").permitAll()
				.anyRequest().authenticated()
		.and()
			.formLogin().loginPage("/login").permitAll()
			.failureUrl("/login_error").permitAll()
		.and()
			.logout()
				.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
				.logoutSuccessUrl("/home")
				.invalidateHttpSession(true)
				.deleteCookies("JSESSIONID");
	}
}

ソースコード抜粋(Java)

UserDetails実装クラス。@overrideアノテーションが付いているメソッドは認証処理上必要なもの(たぶん)。@overrideアノテーションが付いてないメソッド(および関連するフィールド)がユーザ付帯情報。ここでは、メアドや姓名を格納している。
package net.rsquare.r2bingo.userdetail;

import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

public class CustomUserDetails implements UserDetails {
	private static final long serialVersionUID = -4928337998301796817L;
	
	private Long userId;
	private String username;
	private String password;
	private boolean enabled;
	private String familyName;
	private String givenName;
	private String email;
	private Collection<? extends GrantedAuthority> authorities;
	private boolean isUserAdmin;
	
	public CustomUserDetails(Long userId, String username, String password,
			boolean enabled, String familyName, String givenName, String email,
			Collection<? extends GrantedAuthority> authorities, boolean isUserAdmin) {
		this.userId = userId;
		this.username = username;
		this.password = password;
		this.enabled = enabled;
		this.familyName = familyName;
		this.givenName = givenName;
		this.email = email;
		this.authorities = authorities;
		this.isUserAdmin = isUserAdmin;
	}
	

	public Long getUserId() {
		return userId;
	}

	public String getFamilyName() {
		return familyName;
	}

	public String getGivenName() {
		return givenName;
	}

	public String getEmail() {
		return email;
	}

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		return this.authorities;
	}

	@Override
	public String getPassword() {
		return this.password;
	}

	@Override
	public String getUsername() {
		return this.username;
	}

	@Override
	public boolean isAccountNonExpired() {
		return this.enabled;
	}

	@Override
	public boolean isAccountNonLocked() {
		return this.enabled;
	}

	@Override
	public boolean isCredentialsNonExpired() {
		return this.enabled;
	}

	@Override
	public boolean isEnabled() {
		return this.enabled;
	}

	public boolean isUserAdmin() {
		return isUserAdmin;
	}
	public boolean getIsUserAdmin() {
		return isUserAdmin;
	}

	@Override
	public String toString() {
		return "CustomUserDetails [userId=" + userId + ", username=" + username
				+ ", password=" + password + ", familyName=" + familyName
				+ ", givenName=" + givenName + ", email=" + email
				+ ", authorities=" + authorities 
				+ ", isUserAdmin=" + isUserAdmin 
				+ ", isAccountNonExpired()="
				+ isAccountNonExpired() + ", isAccountNonLocked()="
				+ isAccountNonLocked() + ", isCredentialsNonExpired()="
				+ isCredentialsNonExpired() + ", isEnabled()=" + isEnabled()
				+ "]";
	}
	
}

UserDetailsServices実装クラス。
package net.rsquare.r2bingo.userdetail;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import net.rsquare.r2bingo.dao.auth.GroupDao;
import net.rsquare.r2bingo.dao.auth.UserDao;
import net.rsquare.r2bingo.entity.auth.Group;
import net.rsquare.r2bingo.entity.auth.GroupAuthority;
import net.rsquare.r2bingo.entity.auth.User;
import net.rsquare.r2bingo.entity.auth.UserAuthority;

@Service("userDetailsService")
@Transactional
public class CustomUserDetailService implements UserDetailsService {
	private static final Logger logger = LoggerFactory.getLogger(CustomUserDetailService.class);
	
	@Autowired
	private UserDao userDao;
	
	@Autowired
	private GroupDao groupDao;
	
	@Override
	@Transactional(readOnly=true)
	public UserDetails loadUserByUsername(String username)
			throws UsernameNotFoundException {
		List<User> users = userDao.find(username);
		List<Group> groups = groupDao.findByUsername(username);
		if (users == null || users.size() != 1) {
			throw new UsernameNotFoundException("UserName "+username+" not found");
		}
		
		User u = users.get(0);
		List<UserAuthority> authorities = u.getAuthorities();
		final Set<SimpleGrantedAuthority> auths = new HashSet<SimpleGrantedAuthority>();
		boolean isUserAdmin = false;
		for (UserAuthority auth : authorities) {
			String userAuth = auth.getAuthority();
			if (userAuth.equals(UserAuthority.UserAuthorityTypes.admin.name())) {
				isUserAdmin = true;
			}
			logger.debug("UserAuthority : {}, {}", userAuth, UserAuthority.UserAuthorityTypes.admin.name());
			auths.add(new SimpleGrantedAuthority(userAuth));
		}
		
		if (groups != null && groups.size() > 0) {
			for (Group group : groups) {
				List<GroupAuthority> groupAuthorities = group.getGroupAuthorities();
				for (GroupAuthority ga : groupAuthorities) {
					String groupAuth = ga.getAuthority();
					logger.debug("GroupAuthority : {}", groupAuth);
					auths.add(new SimpleGrantedAuthority(groupAuth));
				}
			}
		}
		
		CustomUserDetails userDetails = new CustomUserDetails(
				u.getId(),
				u.getUsername(),
				u.getPassword(),
				u.isEnabled(),
				u.getUserInfo().getFamilyName(),
				u.getUserInfo().getGivenName(),
				u.getUserInfo().getEmail(),
				auths, isUserAdmin
			);
		
		logger.debug(userDetails.toString());
		return userDetails;
	}
}

SecurityContextHolderを利用してUserDetails実装クラスをゲットだぜ
package net.rsquare.r2bingo.web.utils;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;

import net.rsquare.r2bingo.userdetail.CustomUserDetails;

public class WebSecurityUtils {
	private static final Logger logger = LoggerFactory.getLogger(WebSecurityUtils.class);
	
	public static final CustomUserDetails getCurrentUser() {
		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
		if (authentication == null) {
			return null;
		} 
		logger.debug("credintials:{}, principal:{}.",
				(authentication.getCredentials()==null?"no credentials":authentication.getCredentials().toString()),
				(authentication.getPrincipal()==null?"no principal":authentication.getPrincipal().toString())
			);
		try {
			return (CustomUserDetails) authentication.getPrincipal();
		} catch (Exception e) {
			logger.debug(e.getMessage());
			return null;
		}
	}
}

あとはControllerクラスでUserDetails実装クラスを使ったり
package net.rsquare.r2bingo.web;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;

import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.mobile.device.Device;
import org.springframework.mobile.device.DeviceUtils;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.NoHandlerFoundException;

import net.rsquare.r2bingo.entity.auth.Group;
import net.rsquare.r2bingo.entity.auth.GroupMembers;
import net.rsquare.r2bingo.entity.auth.User;
import net.rsquare.r2bingo.service.auth.GroupMembersService;
import net.rsquare.r2bingo.service.auth.UserService;
import net.rsquare.r2bingo.userdetail.CustomUserDetails;
import net.rsquare.r2bingo.web.utils.WebSecurityUtils;

@Controller
public class MyPageController {
	private static final Logger logger = LoggerFactory.getLogger(MyPageController.class);

	@Autowired
	private UserService userService;

	@Autowired
	private GroupMembersService groupMembersService;

	@RequestMapping(value = { "/mypage" })
	public String mypage(HttpServletRequest request, Model model, Locale locale) throws NoHandlerFoundException {
		Device currentDevice = DeviceUtils.getCurrentDevice(request);
		CustomUserDetails ud = WebSecurityUtils.getCurrentUser();
		if (ud == null) {
			ServletServerHttpRequest req = new ServletServerHttpRequest(request);
			throw new NoHandlerFoundException(req.getMethod().name(),
					req.getServletRequest().getRequestURI(),
					req.getHeaders());
		}
		String username = ud.getUsername();
                // 
                // いろいろ処理、、、、
                //
		return "mypage/index";
	}
}

ビューでSpringSecurityの認証関係オブジェクトにアクセスしたり
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<!-- 激しく省略 -->
  <nav class="menu" th:fragment="menu_nav">
  <div class="hello" sec:authorize="isAuthenticated()">    <!-- ログインしていたらこのdiv要素を表示 -->
    <span th:text="${@ms.getMessage('hello.user',new Object[]{#authentication.name},null,#locale)}">no name</span>
  </div>

    <div class="wrap">
    <ul>
      <li class="roundbtn"><a href="#" 
            th:href="@{/home}" 
            th:text="${@ms.getMessage('go.home',null,null,#locale)}">home</a></li>
      <li class="roundbtn"><a href="#" 
            th:href="@{/events}" 
            th:text="${@ms.getMessage('go.events',null,null,#locale)}">events</a></li>
      <li class="roundbtn"><a href="#" 
            th:href="@{/mypage}" 
            th:text="${@ms.getMessage('go.mypage',null,null,#locale)}">mypage</a></li>
      <li class="roundbtn" sec:authorize="hasAuthority('admin')">    <!-- admin 権限 を持っていたらこのli要素を表示 -->
        <a href="#" 
            th:href="@{/manage}" 
            th:text="${@ms.getMessage('go.manage',null,null,#locale)}">manage</a></li>
      <li class="roundbtn" sec:authorize="isAuthenticated()">    <!-- ログインしていたらこのli要素を表示 -->
        <a href="#" 
            th:href="@{/logout}" 
            th:text="${@ms.getMessage('go.logout',null,null,#locale)}">logout</a></li>
    </ul>
    </div>
  </nav>
<!-- 激しく省略 -->
</html>


実行例

上記のソースは、WebSocketデモサイトで使っているので、「イベント」や「マイページ」をクリックして、ログインしてみてね。何にもできないけどさ。
WebSocket ビンゴ

まとめ

  • 上記デモサイト程度の規模だとCustomUserDetailsを作るより、HttpServletRequestのprincipalからusernameだけもらって、Controllerのメソッド内でDB検索するほうが楽チン。
  • 30も40も機能というか業務というか部署があるシステムだと、CustomUserDetails作ったほうが楽チンなのかな。
  • ユーザ情報のリレーションが込み入っているとフツーにDB検索するほうが楽チンそうだ。
  • 白状するとGroupAuthorityをどうやって使えばいいかわかってない。


last modified : 2017-07-11T22:22:26+09:00