Spring SecurityとJWTの組み合わせ
16984 ワード
概要 JWTを使用して権限検証を行うメリットは、Sessionに比べてサーバメモリを大量に消費する必要があり、マルチサーバ時に共有Sessionの問題が発生し、携帯電話などのモバイル端末へのアクセスが面倒な であることです.でJWTはサーバに格納する必要がなく、サーバリソースを占有しない(すなわち無状態である)、ユーザはログイン後にTokenを取得した後、アクセスに必要な権限の要求時にToken(一般的にHttp要求ヘッダに設定されている)を添付し、JWTはマルチサーバ共有の問題もなく、携帯電話のモバイル側アクセスの問題もなく、セキュリティを向上させるためには、TokenをユーザのIPアドレスにバインドできるケースソースコードダウンロード フロントエンドプロセスユーザがAJAXによりログインしてToken を得る.以降アクセスに権限要求が必要な場合はTokenを添付して にアクセスする.
バックエンドプロセス(Spring Boot+Spring Security+JJJWT)
考え方:ユーザ、パーミッションエンティティクラス、およびデータ転送オブジェクト を作成する.は、ユーザ情報 を取得するためのDao層インタフェースを記述する. UserDetails(Securityがサポートするユーザエンティティオブジェクト、権限情報を含む) を実装する. UserDetailsSevice(データベースからユーザ情報を取得し、UserDetailsにパッケージ) を実現する. JWTToken生成ツールを記述し、Token を生成、検証、解析する設定Security、構成要求処理と設定UserDetails取得方式がカスタムのUserDetailsSevice である. LoginControllerを作成し、ユーザーのログイン名パスワードを受信して検証を行い、検証に成功したらTokenに戻ってユーザー に与える.フィルタを作成し、ユーザ要求ヘッダまたはパラメータにTokenが含まれている場合に解析し、Authenticationを生成し、SecurityContextにバインドし、Securityが を使用するようにする.ユーザは、権限を必要とするページにアクセスしたが、正しいTokenが添付されておらず、フィルタ処理時にAuthenticationが生成されず、アクセス権限も存在せず、アクセスできず、Noアクセス成功 .
ユーザーエンティティクラスを作成し、データを挿入します.
User(ユーザ)エンティティークラス
ロールエンティティクラス
データの挿入
Userテーブル
id
name
password
1
linyuan
123
ロールテーブル
id
name
1
USER
User_ROLE表
uid
rid
1
1
Daoレイヤインタフェース、ユーザー名でデータを取得し、Java 8のOptionalオブジェクトを返す
フロントエンドとのデータ転送のためのLoginDTOの作成
Token生成ツールを作成し、JJWTライブラリを使用して作成します.Tokenの生成(Stringに戻る)、Tokenの解析(Authentication認証オブジェクトに戻る)、Tokenの検証(ブール値に戻る)の3つの方法があります.
ユーザーエンティティークラスを代表するUserDetailsインタフェースを実現し、私たちのUserオブジェクトにパッケージを行い、権限などの性質を含み、Spring Securityで使用することができます.
UserDetailsを取得する方法は1つしかありません.データベースからUserオブジェクトを取得し、UserDetailsにパッケージして戻ります.
ユーザーがTokenを携帯するとTokenを取得し、Tokenに基づいてAuthentication認証オブジェクトを生成し、Spring Securityの権限制御のためにSecurityContextに保存するフィルタを作成します.
LoginControllerを作成し、ユーザーはユーザー名、パスワードを通じて/auth/loginにアクセスし、LoginDTOオブジェクトを通じて受信し、Authenticationオブジェクトを作成し、コードの中でU s e r m e P a sswordAuthenticationTokenであり、オブジェクトが存在するかどうかを判断し、AuthenticationManagerのauthenticateメソッドを通じて認証オブジェクトを検証する.AuthenticationManagerの実装クラスProviderManagerはAuthentionProvider(認証処理)で検証され、デフォルトのProviderManagerはDaoAuthenticationProviderを呼び出して認証処理を行い、DaoAuthenticationProviderではUserDetailsService(認証情報ソース)でUserDetailsを取得します.認証が成功すると、権限を含むAuthentionが返され、SecurityContextHolder.getContext().setAuthentication()がSecurityContextに設定され、Authenticationに基づいてTokenが生成され、ユーザーに返されます.
Security構成クラスを作成し、WebSecurityConfigurerAdapterを継承し、configureメソッドを書き換える
テスト用のControllerの作成
Title
var header = "";
function login() {
$.post("http://localhost:8080/auth/login", {
username: $("#username").val(),
password: $("#password").val()
}, function (data) {
console.log(data);
header = data;
})
}
function toUserPageBtn() {
$.ajax({
type: "get",
url: "http://localhost:8080/userpage",
beforeSend: function (request) {
request.setRequestHeader("Authorization", header);
},
success: function (data) {
console.log(data);
}
});
}
バックエンドプロセス(Spring Boot+Spring Security+JJJWT)
考え方:
ユーザーエンティティクラスを作成し、データを挿入します.
User(ユーザ)エンティティークラス
@Data
@Entity
public class User {
@Id
@GeneratedValue
private int id;
private String name;
private String password;
@ManyToMany(cascade = {CascadeType.REFRESH}, fetch = FetchType.EAGER)
@JoinTable(name = "user_role", joinColumns = {@JoinColumn(name = "uid", referencedColumnName = "id")}, inverseJoinColumns = {@JoinColumn(name = "rid", referencedColumnName = "id")})
private List roles;
}
ロールエンティティクラス
@Data
@Entity
public class Role {
@Id
@GeneratedValue
private int id;
private String name;
@ManyToMany(mappedBy = "roles")
private List users;
}
データの挿入
Userテーブル
id
name
password
1
linyuan
123
ロールテーブル
id
name
1
USER
User_ROLE表
uid
rid
1
1
Daoレイヤインタフェース、ユーザー名でデータを取得し、Java 8のOptionalオブジェクトを返す
public interface UserRepository extends Repository {
Optional findByName(String name);
}
フロントエンドとのデータ転送のためのLoginDTOの作成
@Data
public class LoginDTO implements Serializable {
@NotBlank(message = " ")
private String username;
@NotBlank(message = " ")
private String password;
}
Token生成ツールを作成し、JJWTライブラリを使用して作成します.Tokenの生成(Stringに戻る)、Tokenの解析(Authentication認証オブジェクトに戻る)、Tokenの検証(ブール値に戻る)の3つの方法があります.
@Component
public class JWTTokenUtils {
private final Logger log = LoggerFactory.getLogger(JWTTokenUtils.class);
private static final String AUTHORITIES_KEY = "auth";
private String secretKey; //
private long tokenValidityInMilliseconds; //
private long tokenValidityInMillisecondsForRememberMe; //( )
@PostConstruct
public void init() {
this.secretKey = "Linyuanmima";
int secondIn1day = 1000 * 60 * 60 * 24;
this.tokenValidityInMilliseconds = secondIn1day * 2L;
this.tokenValidityInMillisecondsForRememberMe = secondIn1day * 7L;
}
private final static long EXPIRATIONTIME = 432_000_000;
// Token
public String createToken(Authentication authentication, Boolean rememberMe){
String authorities = authentication.getAuthorities().stream() // , USER,ADMIN
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
long now = (new Date()).getTime(); //
Date validity; //
if (rememberMe){
validity = new Date(now + this.tokenValidityInMilliseconds);
}else {
validity = new Date(now + this.tokenValidityInMillisecondsForRememberMe);
}
return Jwts.builder() // Token
.setSubject(authentication.getName()) //
.claim(AUTHORITIES_KEY,authorities) //
.setExpiration(validity) //
.signWith(SignatureAlgorithm.HS512,secretKey) //
.compact();
}
//
public Authentication getAuthentication(String token){
System.out.println("token:"+token);
Claims claims = Jwts.parser() // Token payload
.setSigningKey(secretKey)
.parseClaimsJws(token)
.getBody();
Collection extends GrantedAuthority> authorities =
Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(",")) //
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList()); // GrantedAuthority
User principal = new User(claims.getSubject(), "", authorities);
return new UsernamePasswordAuthenticationToken(principal, "", authorities);
}
// Token
public boolean validateToken(String token){
try {
Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); // Token
return true;
}catch (SignatureException e) { //
log.info("Invalid JWT signature.");
log.trace("Invalid JWT signature trace: {}", e);
} catch (MalformedJwtException e) { //JWT
log.info("Invalid JWT token.");
log.trace("Invalid JWT token trace: {}", e);
} catch (ExpiredJwtException e) { //JWT
log.info("Expired JWT token.");
log.trace("Expired JWT token trace: {}", e);
} catch (UnsupportedJwtException e) { // JWT
log.info("Unsupported JWT token.");
log.trace("Unsupported JWT token trace: {}", e);
} catch (IllegalArgumentException e) { //
log.info("JWT token compact of handler are invalid.");
log.trace("JWT token compact of handler are invalid trace: {}", e);
}
return false;
}
}
ユーザーエンティティークラスを代表するUserDetailsインタフェースを実現し、私たちのUserオブジェクトにパッケージを行い、権限などの性質を含み、Spring Securityで使用することができます.
public class MyUserDetails implements UserDetails{
private User user;
public MyUserDetails(User user) {
this.user = user;
}
@Override
public Collection extends GrantedAuthority> getAuthorities() {
List roles = user.getRoles();
List authorities = new ArrayList<>();
StringBuilder sb = new StringBuilder();
if (roles.size()>=1){
for (Role role : roles){
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return authorities;
}
return AuthorityUtils.commaSeparatedStringToAuthorityList("");
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getName();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
UserDetailsを取得する方法は1つしかありません.データベースからUserオブジェクトを取得し、UserDetailsにパッケージして戻ります.
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//
Optional user = userRepository.findByName(s);
// ,
user.ifPresent((value)->System.out.println(" :"+value.getName()+" :"+value.getPassword()));
// null
return new MyUserDetails(user.orElse(null));
}
}
ユーザーがTokenを携帯するとTokenを取得し、Tokenに基づいてAuthentication認証オブジェクトを生成し、Spring Securityの権限制御のためにSecurityContextに保存するフィルタを作成します.
public class JwtAuthenticationTokenFilter extends GenericFilterBean {
private final Logger log = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
@Autowired
private JWTTokenUtils tokenProvider;
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("JwtAuthenticationTokenFilter");
try {
HttpServletRequest httpReq = (HttpServletRequest) servletRequest;
String jwt = resolveToken(httpReq);
if (StringUtils.hasText(jwt) && this.tokenProvider.validateToken(jwt)) { // JWT
Authentication authentication = this.tokenProvider.getAuthentication(jwt); //
SecurityContextHolder.getContext().setAuthentication(authentication); // SecurityContext
}
filterChain.doFilter(servletRequest, servletResponse);
}catch (ExpiredJwtException e){ //JWT
log.info("Security exception for user {} - {}",
e.getClaims().getSubject(), e.getMessage());
log.trace("Security exception trace: {}", e);
((HttpServletResponse) servletResponse).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
}
}
private String resolveToken(HttpServletRequest request){
String bearerToken = request.getHeader(WebSecurityConfig.AUTHORIZATION_HEADER); // HTTP TOKEN
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")){
return bearerToken.substring(7, bearerToken.length()); // Token , Bearer
}
String jwt = request.getParameter(WebSecurityConfig.AUTHORIZATION_TOKEN); // TOKEN
if (StringUtils.hasText(jwt)) {
return jwt;
}
return null;
}
}
LoginControllerを作成し、ユーザーはユーザー名、パスワードを通じて/auth/loginにアクセスし、LoginDTOオブジェクトを通じて受信し、Authenticationオブジェクトを作成し、コードの中でU s e r m e P a sswordAuthenticationTokenであり、オブジェクトが存在するかどうかを判断し、AuthenticationManagerのauthenticateメソッドを通じて認証オブジェクトを検証する.AuthenticationManagerの実装クラスProviderManagerはAuthentionProvider(認証処理)で検証され、デフォルトのProviderManagerはDaoAuthenticationProviderを呼び出して認証処理を行い、DaoAuthenticationProviderではUserDetailsService(認証情報ソース)でUserDetailsを取得します.認証が成功すると、権限を含むAuthentionが返され、SecurityContextHolder.getContext().setAuthentication()がSecurityContextに設定され、Authenticationに基づいてTokenが生成され、ユーザーに返されます.
@RestController
public class LoginController {
@Autowired
private UserRepository userRepository;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JWTTokenUtils jwtTokenUtils;
@RequestMapping(value = "/auth/login",method = RequestMethod.POST)
public String login(@Valid LoginDTO loginDTO, HttpServletResponse httpResponse) throws Exception{
// Authentication , UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginDTO.getUsername(),loginDTO.getPassword());
//
if (Objects.nonNull(authenticationToken)){
userRepository.findByName(authenticationToken.getPrincipal().toString())
.orElseThrow(()->new Exception(" "));
}
try {
// AuthenticationManager( ProviderManager) authenticate Authentication
Authentication authentication = authenticationManager.authenticate(authenticationToken);
// Authentication SecurityContext
SecurityContextHolder.getContext().setAuthentication(authentication);
// Token
String token = jwtTokenUtils.createToken(authentication,false);
// Token Http
httpResponse.addHeader(WebSecurityConfig.AUTHORIZATION_HEADER,"Bearer "+token);
return "Bearer "+token;
}catch (BadCredentialsException authentication){
throw new Exception(" ");
}
}
}
Security構成クラスを作成し、WebSecurityConfigurerAdapterを継承し、configureメソッドを書き換える
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
public static final String AUTHORIZATION_HEADER = "Authorization";
public static final String AUTHORIZATION_TOKEN = "access_token";
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
//
.userDetailsService(userDetailsService)
//
.passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//
http
// CSRF、CORS
.cors().disable()
.csrf().disable()
// Token, Session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// Http
.authorizeRequests()
//
.antMatchers("/","/auth/login").permitAll()
//
.anyRequest().authenticated()
//
.antMatchers("/userpage").hasAnyRole("USER")
.and()
//
.logout().permitAll();
// JWT filter
http
.addFilterBefore(genericFilterBean(), UsernamePasswordAuthenticationFilter.class);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public GenericFilterBean genericFilterBean() {
return new JwtAuthenticationTokenFilter();
}
}
テスト用のControllerの作成
@RestController
public class UserController {
@PostMapping("/login")
public String login() {
return "login";
}
@GetMapping("/")
public String index() {
return "hello";
}
@GetMapping("/userpage")
public String httpApi() {
System.out.println(SecurityContextHolder.getContext().getAuthentication().getPrincipal());
return "userpage";
}
@GetMapping("/adminpage")
public String httpSuite() {
return "userpage";
}
}