Spring Security系列(三) 您所在的位置:网站首页 security白名单 Spring Security系列(三)

Spring Security系列(三)

#Spring Security系列(三)| 来源: 网络整理| 查看: 265

本文场景: 使用Spring Security做权限控制,系统包含普通用户和系统管理员两种类型,希望有不同的登录入口;并且在Spring Gateway上配置,Gateway使用的是WebFlux,无法兼容MVC,所以使用WebFlux配置Security。纯记录,目前项目还是小demo,贴的示例代码可以正常运行,但是业务逻辑还有点问题。

1. SecurityWebFilterChain配置

创建两个SecurityWebFilterChainbean,管理员配置限制范围为/manage/**,用户将该路径设置为白名单,权限配置不限制范围。

管理员配置必须指定比用户配置更小的@Order,使其具有更高的优先级,能够在用户的SecurityWebFilterChain之前被执行,因为一旦匹配到用户的SecurityWebFilterChain,将不会继续匹配到管理员的配置,管理员的配置将不会生效。具体原因上一个节有分析。

package com.nineya.rkproblem.config; import com.nineya.rkproblem.filter.ManageAuthenticationFilter; import com.nineya.rkproblem.security.AuthenticationFailureHandler; import com.nineya.rkproblem.security.AuthenticationSuccessHandler; import com.nineya.rkproblem.security.ManageAuthenticationManager; import com.nineya.rkproblem.security.UserAuthenticationManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.security.authentication.DelegatingReactiveAuthenticationManager; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.SecurityWebFiltersOrder; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers; import javax.annotation.Resource; import java.util.LinkedList; /** * @author 殇雪话诀别 * 2020/11/7 */ @Configuration @EnableWebFluxSecurity public class SecurityWebFluxConfiguration { /** * 登录成功的处理器 */ @Resource private AuthenticationSuccessHandler successHandler; /** * 登录失败的处理器 */ @Resource private AuthenticationFailureHandler failureHandler; @Resource private UserAuthenticationManager userAuthenticationManager; @Resource private ManageAuthenticationManager manageAuthenticationManager; @Resource private ManageAuthenticationFilter manageAuthenticationFilter; /** * 白名单 */ private static final String[] AUTH_WHITELIST = new String[]{"/manage/login", "/user/login"}; @Bean @Order(0) public SecurityWebFilterChain manageSecurityFilterChain(ServerHttpSecurity http){ http .securityMatcher(ServerWebExchangeMatchers.pathMatchers("/manage/**")) .formLogin(n->n .loginPage("/manage/login") .requiresAuthenticationMatcher(ServerWebExchangeMatchers.pathMatchers("/manage/auth/login")) .authenticationManager(manageAuthenticationManager) .authenticationSuccessHandler(successHandler) .authenticationFailureHandler(failureHandler)) .logout(n->n.logoutUrl("/manage/logout")) .csrf(n->n.disable()) .httpBasic(n->n.disable()) .authorizeExchange(n->n .pathMatchers(AUTH_WHITELIST).permitAll() .pathMatchers("/manage/api/**").hasRole("MANAGE") .anyExchange().authenticated()) .addFilterBefore(manageAuthenticationFilter, SecurityWebFiltersOrder.FORM_LOGIN); return http.build(); } @Bean @Order(1) public SecurityWebFilterChain userSecurityFilterChain(ServerHttpSecurity http){ http .securityMatcher(ServerWebExchangeMatchers.anyExchange()) .formLogin(n->n .loginPage("/user/login") .requiresAuthenticationMatcher(ServerWebExchangeMatchers.pathMatchers("/user/auth/login")) .authenticationManager(userAuthenticationManager) .authenticationSuccessHandler(successHandler) .authenticationFailureHandler(failureHandler)) .logout(n->n.logoutUrl("/user/logout")) .csrf(n->n.disable()) .httpBasic(n->n.disable()) .authorizeExchange(n->n .pathMatchers("/manage/**", "/user/login").permitAll() .pathMatchers("/user/api/**").hasRole("USER")); return http.build(); } @Bean public PasswordEncoder bCryptPasswordEncoder(){ return new BCryptPasswordEncoder(); } } 2. AuthenticationManager配置

两个登录入口需要创建两个AuthenticationManager,本文只是简单示例,能正常跑起来,逻辑不一定通的哦。

package com.nineya.rkproblem.security; import com.nineya.rkproblem.service.ManageDetailsServiceImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Primary; import org.springframework.security.authentication.AbstractUserDetailsReactiveAuthenticationManager; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; /** * @author 殇雪话诀别 * 2020/11/15 */ @Component @Primary public class ManageAuthenticationManager extends AbstractUserDetailsReactiveAuthenticationManager { private Scheduler scheduler = Schedulers.boundedElastic(); @Autowired private ManageDetailsServiceImpl manageDetailsService; @Autowired private PasswordEncoder passwordEncoder; @Override public Mono authenticate(Authentication authentication) { String username = authentication.getName(); String password = String.valueOf(authentication.getCredentials()); return retrieveUser(username) .publishOn(scheduler) .filter(u -> passwordEncoder.matches(password, passwordEncoder.encode(u.getPassword()))) .switchIfEmpty(Mono.defer(() -> Mono.error(new BadCredentialsException("账号或密码错误!")))) .map(u-> new UsernamePasswordAuthenticationToken(u, u.getPassword())); } @Override protected Mono retrieveUser(String username) { return manageDetailsService.findByUsername(username); } } package com.nineya.rkproblem.security; import com.nineya.rkproblem.service.ManageDetailsServiceImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AbstractUserDetailsReactiveAuthenticationManager; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; /** * @author 殇雪话诀别 * 2020/11/8 */ @Component public class UserAuthenticationManager extends AbstractUserDetailsReactiveAuthenticationManager { private Scheduler scheduler = Schedulers.boundedElastic(); @Autowired private ManageDetailsServiceImpl manageDetailsService; @Autowired private PasswordEncoder passwordEncoder; @Override public Mono authenticate(Authentication authentication) { String username = authentication.getName(); String password = String.valueOf(authentication.getCredentials()); return retrieveUser(username) .publishOn(scheduler) .filter(u -> passwordEncoder.matches(password, passwordEncoder.encode(u.getPassword()))) .switchIfEmpty(Mono.defer(() -> Mono.error(new BadCredentialsException("Invalid Credentials")))) .map(u-> new UsernamePasswordAuthenticationToken(u, u.getPassword())); } @Override protected Mono retrieveUser(String username) { return manageDetailsService.findByUsername(username); } } 3. 用户信息服务

在AuthenticationManager中需要指定一个ReactiveUserDetailsService用于查询用户信息,从而比对用户的登录数据是否正确,实现如下:

package com.nineya.rkproblem.service; import com.nineya.rkproblem.client.ManageClient; import com.nineya.rkproblem.entity.Manage; import com.nineya.tool.text.CheckText; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.ReactiveUserDetailsService; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; /** * @author 殇雪话诀别 * 2020/11/8 */ @Component public class ManageDetailsServiceImpl implements ReactiveUserDetailsService { @Autowired private ManageClient manageClient; @Override public Mono findByUsername(String username) { if (!CheckText.checkMail(username)){ return Mono.error(new UsernameNotFoundException(String.format("邮箱 %s 格式错误!", username))); } Manage manage = manageClient.login(username); if (manage == null){ return Mono.error(new UsernameNotFoundException(String.format("账号 %s 不存在!", username))); } UserDetails user = User.withUsername(username) .password(manage.getPassword()) .roles("MANAGE") .build(); return Mono.just(user); } } 4. 登录结果处理器

在本文场景中,登录结果处理和业务无关,所以管理员和用户共用一个处理器。

登录失败处理器:

package com.nineya.rkproblem.security; import com.alibaba.fastjson.JSONObject; import com.nineya.rkproblem.entity.ResponseResult; import com.nineya.rkproblem.entity.ResultCode; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.LockedException; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.web.server.WebFilterExchange; import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler; import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; import java.nio.charset.StandardCharsets; /** * @author 殇雪话诀别 * 2020/11/12 */ @Component public class AuthenticationFailureHandler implements ServerAuthenticationFailureHandler { @Override public Mono onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException exception) { ServerHttpResponse response = webFilterExchange.getExchange().getResponse(); ResponseResult result = new ResponseResult().setError(true); if (exception instanceof UsernameNotFoundException) { // 用户不存在 result.setCode(ResultCode.SUCCESS.getCode()); result.setMessage(exception.getMessage()); } else if (exception instanceof BadCredentialsException) { // 密码错误 result.setCode(ResultCode.SUCCESS.getCode()); result.setMessage(exception.getMessage()); } else if (exception instanceof LockedException) { // 用户被锁 result.setCode(ResultCode.SUCCESS.getCode()); result.setMessage(exception.getMessage()); } else { // 系统错误 result.setCode(ResultCode.SERVER_ERROR.getCode()); result.setMessage(exception.getMessage()); } String body = JSONObject.toJSONString(result); DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8)); return response.writeWith(Mono.just(buffer)); } }

登录成功处理器,在此处可实现通过Authentication中的信息生成token响应给客户端,实现自定义token,可以关闭Security默认的Session方案。但是如果此处自定义token,需要添加一个token验证的过滤器,在每次请求时解析自定义的token创建Authentication。

package com.nineya.rkproblem.security; import com.alibaba.fastjson.JSONObject; import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.interfaces.DecodedJWT; import com.nineya.rkproblem.entity.ResponseResult; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.security.core.Authentication; import org.springframework.security.web.server.WebFilterExchange; import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler; import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; import java.nio.charset.StandardCharsets; import java.util.Date; import java.util.stream.Collectors; /** * @author 殇雪话诀别 * 2020/11/12 */ @Component public class AuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler { @Override public Mono onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) { ServerHttpResponse response = webFilterExchange.getExchange().getResponse(); response.setStatusCode(HttpStatus.OK); Algorithm algorithm = Algorithm.HMAC256("secret"); String token = JWT.create() .withClaim("uid", "123456") .withIssuedAt(new Date()) .withIssuer("rkproblem") .sign(algorithm); JWTVerifier verifier = JWT.require(algorithm) .build(); DecodedJWT jwt = verifier.verify(token); System.out.println(jwt.getClaims().entrySet().stream() .map(n->n.getKey()+" = " + n.getValue().asString()).collect(Collectors.joining(", "))); ResponseResult result = ResponseResult.success(token); String body = JSONObject.toJSONString(result); DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8)); return response.writeWith(Mono.just(buffer)); } } 5. 权限认证过滤器

示例中,简单的打印了一下token,并未将token转换为AuthenticationToken对象。

package com.nineya.rkproblem.filter; import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; import reactor.core.publisher.Mono; /** * 将表单里的参数转为AuthenticationToken对象 * * @author 殇雪话诀别 * 2020/11/20 */ @Component public class ManageAuthenticationFilter implements WebFilter { @Override public Mono filter(ServerWebExchange serverWebExchange, WebFilterChain webFilterChain) { HttpHeaders headers = serverWebExchange.getRequest().getHeaders(); String manageToken = headers.getFirst("MANAGE_TOKEN"); System.out.println(manageToken); return webFilterChain.filter(serverWebExchange); } }


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有