Spring Security+JWT RBAC動的認可の完了
114479 ワード
この記事はspring securityシリーズの最初の記事で、spring securityによってエンタープライズ・プロジェクトの権限制御を完了する方法と、RedisによってJWTの失効を制御する方法に重点を置いています.
1.RBACとは
RBAC(Role-Based Access Control)は、ロールの権限制御に基づいて、ロールに権限が関連付けられ、ユーザは、適切なロールのメンバーになることによって、これらのロールの権限を得る.
2.JWTとSpring Security
Spring securityライセンスは主に2つに分けられ、1つはsecurity内部でログインユーザーのメンテナンスを担当するセッションであり、1つはJWT方式を採用し、セッションを管理しない.JWTとSecurityの詳細については、仲間たちが自分で調べてください(関連サイトのお勧め:http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html)ここでは説明しませんので、本文を始めましょう.
3.1依存のインポート
3.securityコア構成クラス:WebSecurityConfig
1.
2.
3.
4.JWTの認証フィルタ
このフィルタは、システムにアクセスするすべての要求をブロックするため、ログインとログインを含む無視されたすべてのURLを解放し、検証されたJWTのユーザー情報をauthenticationにカプセル化する必要があります.
5.RBAC権限照合器
この方法は多くのように見えますが、実際にはページリクエストのURLとユーザーが所有するすべての権限リソース(URL)を一致させることしかできません.
6.
7.JWTのリフレッシュおよびログインユーザのログアウト
周知のように、JWTは無状態であり、サービス側はJWTを解析することによってユーザが事前にログアウトしたかどうかを知ることができないため、Redisの期限切れメカニズムを利用して、ユーザに終了を通知する目的を達成する.JWTが作成されると、生成されたJWTはユーザ名を接頭辞としてRedisに格納され、終了するとRedisのこのユーザ名のJWTをクリアし、アクセスするたびにJWTを解析し、Redisにこのユーザ名のJWTがまだ存在するかどうかを判断し、存在しない場合は、このユーザがログアウトしたことを示す.JWTの再署名です.ここではrefresh_を採用しています.tokenの形式と、ログイン時に2つのJWT、1つのtoken、1つのrefresh_を作成します.token,refresh_tokenの有効期限の設定は比較的に長く、tokenが無効になった後、フロントエンドはrefreshを呼び出すことができます.tokenのインタフェースをリフレッシュして、再署名の目的を達成します.
ログインとログインの方法
7.効果テスト
8.データベースとソース
上はプロジェクトの一部のコアコードだけで、完全なコードとデータベースはすでにGithubに管理されています.ソースリンクにアクセスして自分でダウンロードしてください.役に立つと思ったら、starを覚えてください.何か問題があれば、issuesやメールで交流してください.
1.RBACとは
RBAC(Role-Based Access Control)は、ロールの権限制御に基づいて、ロールに権限が関連付けられ、ユーザは、適切なロールのメンバーになることによって、これらのロールの権限を得る.
2.JWTとSpring Security
Spring securityライセンスは主に2つに分けられ、1つはsecurity内部でログインユーザーのメンテナンスを担当するセッションであり、1つはJWT方式を採用し、セッションを管理しない.JWTとSecurityの詳細については、仲間たちが自分で調べてください(関連サイトのお勧め:http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html)ここでは説明しませんので、本文を始めましょう.
3.1依存のインポート
<!-- spring security jwt -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
3.securityコア構成クラス:WebSecurityConfig
@Configuration
@EnableWebSecurity
@EnableConfigurationProperties(CustomConfig.class)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomConfig customConfig;
@Autowired
private SecurityUserService securityUserService;
@Autowired
@Qualifier("securityAccessDeniedHandler")
private AccessDeniedHandler securityAccessDeniedHandler;
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(securityUserService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors()
// CSRF
.and().csrf().disable()
// , LoginController#login
.formLogin().disable()
.httpBasic().disable()
//
.authorizeRequests()
//
.anyRequest()
.authenticated()
// RBAC url
.anyRequest()
.access("@rbacAuthorityService.hasPermission(request,authentication)")
// , LoginController#logout
.and().logout().disable()
//
.exceptionHandling().accessDeniedHandler(securityAccessDeniedHandler);
// Session
http.sessionManagement()
// JWT, Session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// JWT
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
/**
* , AuthController
* {@link #configure(HttpSecurity)}
* {@code http.authorizeRequests().antMatchers("/api/auth/**").permitAll()}
*/
@Override
public void configure(WebSecurity web) {
WebSecurity and = web.ignoring().and();
// GET
customConfig.getIgnores().getGet().forEach(url -> and.ignoring().antMatchers(HttpMethod.GET, url));
// POST
customConfig.getIgnores().getPost().forEach(url -> and.ignoring().antMatchers(HttpMethod.POST, url));
// DELETE
customConfig.getIgnores().getDelete().forEach(url -> and.ignoring().antMatchers(HttpMethod.DELETE, url));
// PUT
customConfig.getIgnores().getPut().forEach(url -> and.ignoring().antMatchers(HttpMethod.PUT, url));
// HEAD
customConfig.getIgnores().getHead().forEach(url -> and.ignoring().antMatchers(HttpMethod.HEAD, url));
// PATCH
customConfig.getIgnores().getPatch().forEach(url -> and.ignoring().antMatchers(HttpMethod.PATCH, url));
// OPTIONS
customConfig.getIgnores().getOptions().forEach(url -> and.ignoring().antMatchers(HttpMethod.OPTIONS, url));
// TRACE
customConfig.getIgnores().getTrace().forEach(url -> and.ignoring().antMatchers(HttpMethod.TRACE, url));
//
customConfig.getIgnores().getPattern().forEach(url -> and.ignoring().antMatchers(url));
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
1.
AuthenticationManagerBuilder auth
は主にsecurityの暗号化方式を設置している(BCryptPasswordEncoder
も現在比較的流行している安全な暗号化方式であり、MD 5よりも効率が高い)、userDetailsService
はユーザー名、パスワードの検証と授権を担当している.2.
HttpSecurity http
は主にsecurityコアフィルタチェーンの配置であり、登録、登録、異常などのプロセッサを配置することができる.JWT方式を採用しているため、securityが提供する登録と登録を無効にし、JWTのフィルタを配置し、RBAC検査の方式を配置した.3.
WebSecurity web
は主に、プロファイルに設定されたローのURLをcustomConfigで読み取るsecurityローのパスの構成を担当します.4.JWTの認証フィルタ
JwtAuthenticationFilter
の構成/**
* @author lirong
* @ClassName: JwtAuthenticationFilter
* @Description: Jwt
* @date 2019-07-12 9:50
*/
@Component
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private SecurityUserService userDetailsService;
@Autowired
private JwtUtil jwtUtil;
@Autowired
private CustomConfig customConfig;
@Autowired
private IApplicationConfig applicationConfig;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
//
if (checkIgnores(request)) {
chain.doFilter(request, response);
return;
}
String jwt = jwtUtil.getJwtFromRequest(request);
if (StrUtil.isNotBlank(jwt)) {
try {
String username = jwtUtil.getUsernameFromJWT(jwt, false);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
} catch (CustomException e) {
ResponseUtils.renderJson(request, response, e, applicationConfig.getOrigins());
}
} else {
ResponseUtils.renderJson(request, response, ResultCode.UNAUTHORIZED, null, applicationConfig.getOrigins());
}
}
/**
*
* @param request
* @return true - ,false -
*/
private boolean checkIgnores(HttpServletRequest request) {
String method = request.getMethod();
HttpMethod httpMethod = HttpMethod.resolve(method);
if (ObjectUtil.isNull(httpMethod)) {
httpMethod = HttpMethod.GET;
}
Set<String> ignores = Sets.newHashSet();
switch (httpMethod) {
case GET:
ignores.addAll(customConfig.getIgnores()
.getGet());
break;
case PUT:
ignores.addAll(customConfig.getIgnores()
.getPut());
break;
case HEAD:
ignores.addAll(customConfig.getIgnores()
.getHead());
break;
case POST:
ignores.addAll(customConfig.getIgnores()
.getPost());
break;
case PATCH:
ignores.addAll(customConfig.getIgnores()
.getPatch());
break;
case TRACE:
ignores.addAll(customConfig.getIgnores()
.getTrace());
break;
case DELETE:
ignores.addAll(customConfig.getIgnores()
.getDelete());
break;
case OPTIONS:
ignores.addAll(customConfig.getIgnores()
.getOptions());
break;
default:
break;
}
ignores.addAll(customConfig.getIgnores()
.getPattern());
if (CollUtil.isNotEmpty(ignores)) {
for (String ignore : ignores) {
AntPathRequestMatcher matcher = new AntPathRequestMatcher(ignore, method);
if (matcher.matches(request)) {
return true;
}
}
}
return false;
}
}
このフィルタは、システムにアクセスするすべての要求をブロックするため、ログインとログインを含む無視されたすべてのURLを解放し、検証されたJWTのユーザー情報をauthenticationにカプセル化する必要があります.
5.RBAC権限照合器
/**
* @author lirong
* @ClassName: JwtAuthenticationFilter
* @Description: Jwt
* @date 2019-07-12 9:50
*/
@Slf4j
@Component
public class RbacAuthorityService {
@Autowired
private SecurityUserService userDetails;
@Autowired
private RequestMappingHandlerMapping mapping;
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
checkRequest(request);
Object userInfo = authentication.getPrincipal();
boolean hasPermission = false;
if (userInfo instanceof UserDetails) {
SecurityUser principal = (SecurityUser) userInfo;
SecurityUser userDTO = (SecurityUser) this.userDetails.loadUserByUsername(principal.getUsername());
// , , ,
List<MenuRight> btnPerms = userDTO.getMenus().stream()
//
.filter(menuRight -> menuRight.getGrades() >= 3)
// URL
.filter(menuRight -> StrUtil.isNotBlank(menuRight.getUrl()))
// METHOD
.collect(Collectors.toList());
for (MenuRight btnPerm : btnPerms) {
AntPathRequestMatcher antPathMatcher = new AntPathRequestMatcher(btnPerm.getUrl(), btnPerm.getMethod());
if (antPathMatcher.matches(request)) {
hasPermission = true;
break;
}
}
return hasPermission;
} else {
return false;
}
}
/**
*
*
* @param request
*/
private void checkRequest(HttpServletRequest request) {
// request
String currentMethod = request.getMethod();
Multimap<String, String> urlMapping = allUrlMapping();
for (String uri : urlMapping.keySet()) {
// AntPathRequestMatcher url
// 2 AntPathRequestMatcher
// 1:new AntPathRequestMatcher(uri,method) , , , 2
// 2:new AntPathRequestMatcher(uri) ,
AntPathRequestMatcher antPathMatcher = new AntPathRequestMatcher(uri);
if (antPathMatcher.matches(request)) {
if (!urlMapping.get(uri)
.contains(currentMethod)) {
throw new CustomException(ResultCode.HTTP_BAD_METHOD);
} else {
return;
}
}
}
throw new CustomException(ResultCode.REQUEST_NOT_FOUND);
}
/**
* URL Mapping, {"/test":["GET","POST"],"/sys":["GET","DELETE"]}
*
* @return {@link ArrayListMultimap} URL Mapping
*/
private Multimap<String, String> allUrlMapping() {
Multimap<String, String> urlMapping = ArrayListMultimap.create();
// url
Map<RequestMappingInfo, HandlerMethod> handlerMethods = mapping.getHandlerMethods();
handlerMethods.forEach((k, v) -> {
// key URL
Set<String> url = k.getPatternsCondition()
.getPatterns();
RequestMethodsRequestCondition method = k.getMethodsCondition();
// URL
url.forEach(s -> urlMapping.putAll(s, method.getMethods()
.stream()
.map(Enum::toString)
.collect(Collectors.toList())));
});
return urlMapping;
}
}
この方法は多くのように見えますが、実際にはページリクエストのURLとユーザーが所有するすべての権限リソース(URL)を一致させることしかできません.
6.
UserDetailsService
データベースユーザー情報の照会/**
* @author lirong
* @Date 2019-7-14 22:46:54
* @Desc
*/
@Component("securityUserService")
public class SecurityUserService implements UserDetailsService {
@Resource
private UserMapper userMapper;
@Resource
private RoleMapper roleMapper;
@Resource
private MenuRightMapper menuRightMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDTO userDTO = userMapper.getRolesByUsername(username);
// ID 1
if (null != userDTO){
if(1L == userDTO.getId()) {
this.getAdminPermission(userDTO);
}
SecurityUser securityUser = new SecurityUser(LoginUserDTO.user2LoginUserDTO(userDTO));
return securityUser;
} else {
throw new UsernameNotFoundException(username + " !");
}
}
/**
*
* @param userDTO
* @return
*/
private UserDTO getAdminPermission(UserDTO userDTO) {
List<Role> roles = roleMapper.selectAll();
List<MenuRight> menuRights = menuRightMapper.selectAll();
userDTO.setRoles(roles);
userDTO.setMenus(menuRights);
return userDTO;
}
}
7.JWTのリフレッシュおよびログインユーザのログアウト
周知のように、JWTは無状態であり、サービス側はJWTを解析することによってユーザが事前にログアウトしたかどうかを知ることができないため、Redisの期限切れメカニズムを利用して、ユーザに終了を通知する目的を達成する.JWTが作成されると、生成されたJWTはユーザ名を接頭辞としてRedisに格納され、終了するとRedisのこのユーザ名のJWTをクリアし、アクセスするたびにJWTを解析し、Redisにこのユーザ名のJWTがまだ存在するかどうかを判断し、存在しない場合は、このユーザがログアウトしたことを示す.JWTの再署名です.ここではrefresh_を採用しています.tokenの形式と、ログイン時に2つのJWT、1つのtoken、1つのrefresh_を作成します.token,refresh_tokenの有効期限の設定は比較的に長く、tokenが無効になった後、フロントエンドはrefreshを呼び出すことができます.tokenのインタフェースをリフレッシュして、再署名の目的を達成します.
/**
* jwt
* @author daiyp
* @date 2018-9-26
*/
@EnableConfigurationProperties(JwtConfig.class)
@Configuration
@Slf4j
public class JwtUtil {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private JwtConfig jwtConfig;
/**
* JWT
*
* @param authentication
* @param rememberMe
* @return JWT
*/
public String createJWT(Authentication authentication, Boolean rememberMe, Boolean isRefresh) {
SecurityUser user = (SecurityUser) authentication.getPrincipal();
return createJWT(isRefresh, rememberMe, user.getId(), user.getUsername(), user.getRoles(), user.getMenus(), user.getAuthorities());
}
/**
* JWT
*
* @param id id
* @param subject
* @param roles
* @param authorities
* @return JWT
*/
public String createJWT(Boolean isRefresh,
Boolean rememberMe,
Long id,
String subject,
List<Role> roles,
List<MenuRight> menus,
Collection<? extends GrantedAuthority> authorities) {
Date now = new Date();
JwtBuilder builder = Jwts.builder()
.setId(id.toString())
.setSubject(subject)
.setIssuedAt(now)
.signWith(SignatureAlgorithm.HS256, jwtConfig.getSecret())
.claim("roles", roles)
// .claim("perms", menus)
.claim("authorities", authorities);
//
Long ttl = rememberMe ? jwtConfig.getRemember() : jwtConfig.getTtl();
String redisKey;
if (isRefresh){
ttl *= 3;
redisKey = Constant.REDIS_JWT_REFRESH_TOKEN_KEY_PREFIX + subject;
}else{
redisKey = Constant.REDIS_JWT_TOKEN_KEY_PREFIX + subject;
}
if (ttl > 0) {
builder.setExpiration(DateUtil.offsetMillisecond(now, ttl.intValue()));
}
String jwt = builder.compact();
// JWT Redis
redisTemplate.opsForValue().set(redisKey, jwt, ttl, TimeUnit.MILLISECONDS);
return jwt;
}
/**
* JWT
*
* @param jwt JWT
* @return {@link Claims}
*/
public Claims parseJWT(String jwt, Boolean isRefresh) {
try {
Claims claims = Jwts.parser()
.setSigningKey(jwtConfig.getSecret())
.parseClaimsJws(jwt)
.getBody();
String username = claims.getSubject();
String redisKey = (isRefresh ? Constant.REDIS_JWT_REFRESH_TOKEN_KEY_PREFIX : Constant.REDIS_JWT_TOKEN_KEY_PREFIX)
+ username;
// redis JWT
Long expire = redisTemplate.getExpire(redisKey, TimeUnit.MILLISECONDS);
if (Objects.isNull(expire) || expire <= 0) {
throw new CustomException(ResultCode.TOKEN_EXPIRED);
}
// redis JWT , / , JWT
String redisToken = (String) redisTemplate.opsForValue().get(redisKey);
if (!StrUtil.equals(jwt, redisToken)) {
throw new CustomException(ResultCode.TOKEN_OUT_OF_CTRL);
}
return claims;
} catch (ExpiredJwtException e) {
log.error("Token ");
throw new CustomException(ResultCode.TOKEN_EXPIRED);
} catch (UnsupportedJwtException e) {
log.error(" Token");
throw new CustomException(ResultCode.TOKEN_PARSE_ERROR);
} catch (MalformedJwtException e) {
log.error("Token ");
throw new CustomException(ResultCode.TOKEN_PARSE_ERROR);
} catch (IllegalArgumentException e) {
log.error("Token ");
throw new CustomException(ResultCode.TOKEN_PARSE_ERROR);
}
}
/**
* JWT
*
* @param request
*/
public void invalidateJWT(HttpServletRequest request) {
String jwt = getJwtFromRequest(request);
String username = getUsernameFromJWT(jwt, false);
// redis JWT
redisTemplate.delete(Constant.REDIS_JWT_REFRESH_TOKEN_KEY_PREFIX + username);
redisTemplate.delete(Constant.REDIS_JWT_TOKEN_KEY_PREFIX + username);
}
/**
* request header JWT
*
* @param request
* @return JWT
*/
public String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
/**
* jwt
*
* @param jwt JWT
* @return
*/
public String getUsernameFromJWT(String jwt, Boolean isRefresh) {
Claims claims = parseJWT(jwt, isRefresh);
return claims.getSubject();
}
public Map<String, String> refreshJWT(String token) {
Claims claims = parseJWT(token, true);
//
Date lastTime = claims.getExpiration();
// 1. refreshToken
if (!new Date().before(lastTime)){
throw new CustomException(ResultCode.TOKEN_EXPIRED);
}
// 2. redis token refreshToken
String username = claims.getSubject();
// redisTemplate.delete(Constant.REDIS_JWT_REFRESH_TOKEN_KEY_PREFIX + username);
// redisTemplate.delete(Constant.REDIS_JWT_TOKEN_KEY_PREFIX + username);
// 3. token refreshToken redis
String jwtToken = createJWT(false, false, Long.parseLong(claims.getId()), username,
(List<Role>) claims.get("roles"), null, (Collection<? extends GrantedAuthority>) claims.get("authorities"));
String refreshJwtToken = createJWT(true, false, Long.parseLong(claims.getId()), username,
(List<Role>) claims.get("roles"), null, (Collection<? extends GrantedAuthority>) claims.get("authorities"));
Map<String, String> map = new HashMap<>();
map.put("token", jwtToken);
map.put("refreshToken", refreshJwtToken);
return map;
}
/**
*
* : jwt token
* @param name
* @param param
* @param secret
* @param expirationtime (5 5*60*1000)
* @return
*
*/
public static String sign(String name, Map<String,Object> param, String secret, Long expirationtime){
String JWT = Jwts.builder()
.setClaims(param)
.setSubject(name)
.setExpiration(new Date(System.currentTimeMillis() + expirationtime))
.signWith(SignatureAlgorithm.HS256,secret)
.compact();
return JWT;
}
/**
*
* : jwt
* @param JWT token
* @param secret
* @return
* @exception
*
*/
public static Claims verify(String JWT, String secret){
Claims claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(JWT)
.getBody();
return claims;
}
public static Object getValueFromToken(String jwt,String key, String secret){
return verify(jwt, secret).get(key);
}
}
ログインとログインの方法
/**
* @author lirong
* @ClassName: LoginController
* @Description: Controller
* @date 2019-07-12 9:31
*/
@Slf4j
@RestController
@RequestMapping("/")
public class LoginController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtUtil jwtUtil;
/**
*
*/
@PostMapping("/login")
public RestResult login(@RequestParam String username,
@RequestParam String password,
@RequestParam(required = false, defaultValue = "false") Boolean rememberMe,
HttpServletRequest request,
HttpServletResponse response) {
Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
SecurityContextHolder.getContext()
.setAuthentication(authentication);
String jwt = jwtUtil.createJWT(authentication, rememberMe, false);
String jwt_refresh = jwtUtil.createJWT(authentication, rememberMe, true);
Map<String, String> map = new HashMap<>();
map.put("token", jwt);
map.put("refreshToken", jwt_refresh);
CookieUtils.setCookie(response, "localhost", jwt);
return ResultGenerator.genSuccessResult().setMessage(" ").setData(map);
}
/**
*
* @param request
* @return
*/
@PostMapping("/logout")
public RestResult logout(HttpServletRequest request) {
try {
// JWT
jwtUtil.invalidateJWT(request);
} catch (CustomException e) {
throw new CustomException(ResultCode.UNAUTHORIZED);
}
return ResultGenerator.genSuccessResult().setMessage(" ");
}
/**
* token
* @param refreshToken
* @return
*/
@PostMapping("/refresh/token")
public RestResult refreshToken(String refreshToken) {
Map<String, String> map;
try {
//
map = jwtUtil.refreshJWT(refreshToken);
} catch (CustomException e) {
throw new CustomException(ResultCode.TOKEN_EXPIRED);
}
return ResultGenerator.genSuccessResult().setMessage("token ").setData(map);
}
}
7.効果テスト
8.データベースとソース
上はプロジェクトの一部のコアコードだけで、完全なコードとデータベースはすでにGithubに管理されています.ソースリンクにアクセスして自分でダウンロードしてください.役に立つと思ったら、starを覚えてください.何か問題があれば、issuesやメールで交流してください.