Spring Boot集成Spring Security 搭建登录模块

环境介绍

Spring Boot 3.1.8 + Spring Cloud 2022.0.5 + Spring Security 6.1.6

Spring Security

1. Spring Security 原理

1.1 概念

认证:就是可不可以登录系统
授权:有没有权限访问资源
权限模型:RBAC

1.2 流程图

引用同学流程图https://www.jb51.net/article/283381.htm#_label0,感谢

2.Spring Security 搭建

2.1 项目目录介绍

main 父级工程
----parent 版本控制
----core 核心框架 依赖jar包
----common 公共依赖
----gateway 网关
----plateform 客户业务模块
----admin 后台模块
在本文中有两套登录系统一套为客户登录(plateform),一套为运营登录(admin)
两套登录复用一个core中的登录,并采用用户名密码和手机验证码两种登录方式

2.2 添加依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
            <scope>compile</scope>
        </dependency>

2.3 自定义 UserDetail

@Data
public class User implements UserDetails {
    private Long userId;

    private String username;
    
    private String password;

    private Long organizationId;

    private Collection<? extends GrantedAuthority> authorities = List.of();

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

    public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
        this.authorities = authorities;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }`
}
public interface UserService extends UserDetailsService {

 UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

2.4 配置Spring Security 忽略Path

可以使用使用配置类配置,在本文中将config相关配置放在了登录接口中实线

@Component
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
   //处理登录认证
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //登录过程处理
        http.formLogin()    //表单登录
                .loginProcessingUrl("/api/user/login") //登录请求url地址
                .successHandler(loginSuccessHandler)   //认证成功
                .failureHandler(loginFailureHandler)   //认证失败
                .and()
                .csrf().disable()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS) //不创建Session
                .and().authorizeRequests() //设置需要拦截的请求
                .antMatchers("/api/user/login").permitAll()//登录放行
                .anyRequest().authenticated()  //其他请求一律拦截
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(anonymousAuthenticationHandler)  //匿名无权限类
                .accessDeniedHandler(customerAccessDeniedHandler)       //认证用户无权限
                .and()
                .cors();//支持跨域
    }

本文配置白名单

@Configuration(proxyBeanMethods = false)
public class WebSecurityCustomizerConfig {
    @Autowired
    private IgnorePatternsProperty ignorePatternsProperty;
    @Bean
    public Collection<RequestMatcher> requestIgnoreRequestMatchers() {
        Set<String> patterns = new HashSet<>();

        if (ignorePatternsProperty != null &&
            ignorePatternsProperty.getPatterns() != null) {
            patterns.addAll(ignorePatternsProperty.getPatterns());
        }
        List<RequestMatcher> requestMatchers = new ArrayList<>();
        for (String str : patterns) {
            requestMatchers.add(AntPathRequestMatcher.antMatcher(str));
        }
        return requestMatchers;
    }
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        Collection<RequestMatcher> requestMatchers = requestIgnoreRequestMatchers();

        return new WebSecurityCustomizer() {
            @Override
            public void customize(WebSecurity web) {
                if (requestMatchers.isEmpty()) {
                    return;
                }
                web.ignoring()
                    .requestMatchers(requestMatchers.toArray(new RequestMatcher[0]));
            }
        };
    }
    @Bean
    public WebMvcProperties webMvcProperties() {
        WebMvcProperties properties = new WebMvcProperties();
        // 设置其他属性,例如视图解析器、静态资源位置等
        properties.setThrowExceptionIfNoHandlerFound(true);
        return properties;
    }

2.5 编写登录接口

public abstract class AbstractAuthServerController {

    private final AuthenticationManager authenticationManager;

    public AbstractAuthServerController(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    protected ClaimUser authenticate(Authentication authenticationToken) {

        Authentication authentication = authenticationManager.authenticate(authenticationToken);

        User authenticatedUser = (User) authentication.getPrincipal();

        ClaimUser claimUser = new ClaimUser();
        claimUser.setId(authenticatedUser.getUserId());
        claimUser.setUsername(authenticatedUser.getUsername());
        claimUser.setOrganizationId(authenticatedUser.getOrganizationId());
        claimUser.setAuthorities(AuthorityUtils.authorityListToSet(authenticatedUser.getAuthorities()));

        return claimUser;
    }
}
@Slf4j
@Tag(name = "DefaultAuthServerController")
@RequestMapping("/auth")
@RestController
@Validated
@Observed(name = "DefaultAuthServerController")
public class DefaultAuthServerController extends AbstractAuthServerController {

    private final JwtService jwtService;
    private final UserService userService;
    private final ModelMapper modelMapper;

    public DefaultAuthServerController(
        JwtService jwtService,
        AuthenticationManager authenticationManager,
        UserService userService, ModelMapper modelMapper) {
        super(authenticationManager);
        this.jwtService = jwtService;
        this.userService = userService;
        this.modelMapper = modelMapper;
    }

    @PostMapping("/login")
    public ResponseEntity<LoginResponse> login(@RequestBody @Validated LoginUsernameRequest request) {
        userService.preUsernameLoginCheck(request);

        try {
            ClaimUser claimUser = super.authenticate(
                new UsernameAuthenticationToken(request.getUsername(), request.getPassword(), request.getExtraInfo())
            );

            Map<String, String> jwtTokenPair = jwtService.generateToken(claimUser);

            LoginResponse response = new LoginResponse();

            response.setTokenType(Constant.DEFAULT_TOKEN_TYPE);
            response.setAccessToken(jwtTokenPair.get(Constant.ACCESS_TOKEN));
            response.setAccessExpiresIn(jwtService.getAccessExpirationTime());
            response.setRefreshToken(jwtTokenPair.get(Constant.REFRESH_TOKEN));

            userService.usernameLoginSuccessHandle(request, claimUser);

            return ResponseEntity.ok(response);
        } catch (Exception ex) {
            log.error("Username login failed. message = {}", ex.getMessage());
            userService.usernameLoginFailedHandle(request, ex);
            throw ex;
        }
    }

    @PostMapping(value = "/refresh", consumes = MediaType.TEXT_PLAIN_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<LoginResponse> refresh(@RequestBody @NotBlank(message = "{act.refresh.token.not.blank}") String token) {

        var claim = jwtService.extractAllClaims(token);
        if (!jwtService.isTokenValid(token, claim.getSubject())) {
            throw new ActBadRequestException(MessageSourceContext.getAccessor().getMessage("act.refresh.token.invalid"));
        }
        User user = userService.findById(Long.parseLong(claim.getSubject()));

        ClaimUser claimUser = modelMapper.map(user, ClaimUser.class);
        claimUser.setId(user.getUserId());

        Map<String, String> jwtTokenPair = jwtService.generateToken(claimUser);

        LoginResponse response = new LoginResponse();

        response.setTokenType(Constant.DEFAULT_TOKEN_TYPE);
        response.setAccessToken(jwtTokenPair.get(Constant.ACCESS_TOKEN));
        response.setAccessExpiresIn(jwtService.getAccessExpirationTime());
        response.setRefreshToken(jwtTokenPair.get(Constant.REFRESH_TOKEN));

        return ResponseEntity.ok(response);
    }
@Slf4j
@Tag(name = "SmsAuthServerController")
@RequestMapping("/auth/sms")
@RestController
@Observed(name = "SmsAuthServerController")
public class SmsAuthServerController extends AbstractAuthServerController {

    private final JwtService jwtService;
    private final UserService userService;

    public SmsAuthServerController(JwtService jwtService,
                                   AuthenticationManager authenticationManager,
                                   UserService userService) {
        super(authenticationManager);
        this.jwtService = jwtService;
        this.userService = userService;
    }

    @PostMapping("/login")
    public ResponseEntity<LoginResponse> authenticate(@RequestBody @Validated LoginSmsVerifyCodeRequest request) {
        userService.preSMSLoginCheck(request);

        try {
            ClaimUser claimUser = super.authenticate(
                new SmsAuthenticationToken(request.getPhoneNumber(), request.getVerifyCode(), request.getExtraInfo())
            );

            Map<String, String> jwtTokenPair = jwtService.generateToken(claimUser);

            LoginResponse response = new LoginResponse();

            response.setTokenType(Constant.DEFAULT_TOKEN_TYPE);
            response.setAccessToken(jwtTokenPair.get(Constant.ACCESS_TOKEN));
            response.setAccessExpiresIn(jwtService.getAccessExpirationTime());
            response.setRefreshToken(jwtTokenPair.get(Constant.REFRESH_TOKEN));

            userService.smsLoginSuccessHandle(request, claimUser);

            return ResponseEntity.ok(response);
        } catch (Exception ex) {
            log.error("SMS login failed. message = {}", ex.getMessage());
            userService.smsLoginFailedHandle(request, ex);
            throw ex;
        }
    }

2.6 进入认证流程

username认证流程
  1. 将请求信息封装到Authentication实现UserNameAuthenticationToken
    调用AuthenticationManage下的ProviderManager.authenticate(UserNameAuthenticationToken)
  2. 调用provider.authenticate委托认证AuthenticationProvider
  3. 实现AuthenticationProvider,重写authenticate()方法
  4. 调用 retrieveUser(),在方法里查询user信息返回UserDetails
2.6.1 封装认证信息
public class UsernameAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    private final Object principal;

    private Object credentials;

    private final Object extraInfo;

    /**
     * This constructor can be safely used by any code that wishes to create a
     * <code>UsernameAuthenticationToken</code>, as the {@link #isAuthenticated()}
     * will return <code>false</code>.
     */
    public UsernameAuthenticationToken(Object principal, Object credentials, Object extraInfo) {
        super(null);
        this.principal = principal;
        this.credentials = credentials;
        this.extraInfo = extraInfo;
        setAuthenticated(false);
    }

    /**
     * This constructor should only be used by <code>AuthenticationManager</code> or
     * <code>AuthenticationProvider</code> implementations that are satisfied with
     * producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
     * authentication token.
     *
     * @param principal
     * @param credentials
     * @param authorities
     */
    public UsernameAuthenticationToken(Object principal, Object credentials, Object extraInfo,
                                       Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        this.extraInfo = extraInfo;
        super.setAuthenticated(true); // must use super, as we override
    }

    /**
     * This factory method can be safely used by any code that wishes to create a
     * unauthenticated <code>UsernameAuthenticationToken</code>.
     *
     * @param principal
     * @param credentials
     * @return UsernameAuthenticationToken with false isAuthenticated() result
     * @since 5.7
     */
    public static UsernameAuthenticationToken unauthenticated(Object principal, Object credentials, Object extraInfo) {
        return new UsernameAuthenticationToken(principal, credentials, extraInfo);
    }

    /**
     * This factory method can be safely used by any code that wishes to create a
     * authenticated <code>UsernameAuthenticationToken</code>.
     *
     * @param principal
     * @param credentials
     * @param extraInfo
     * @return UsernameAuthenticationToken with true isAuthenticated() result
     * @since 5.7
     */
    public static UsernameAuthenticationToken authenticated(Object principal, Object credentials, Object extraInfo,
                                                                    Collection<? extends GrantedAuthority> authorities) {
        return new UsernameAuthenticationToken(principal, credentials, extraInfo, authorities);
    }

    @Override
    public Object getCredentials() {
        return this.credentials;
    }

    @Override
    public Object getPrincipal() {
        return this.principal;
    }

    public String getExtraInfo() {
        return extraInfo == null ? null : extraInfo.toString();
    }

    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        Assert.isTrue(!isAuthenticated,
            "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        super.setAuthenticated(false);
    }

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
        this.credentials = null;
    }

}
@Slf4j
@Tag(name = "SmsAuthServerController")
@RequestMapping("/auth/sms")
@RestController
@Observed(name = "SmsAuthServerController")
public class SmsAuthServerController extends AbstractAuthServerController {

    private final JwtService jwtService;
    private final UserService userService;

    public SmsAuthServerController(JwtService jwtService,
                                   AuthenticationManager authenticationManager,
                                   UserService userService) {
        super(authenticationManager);
        this.jwtService = jwtService;
        this.userService = userService;
    }

    @PostMapping("/login")
    public ResponseEntity<LoginResponse> authenticate(@RequestBody @Validated LoginSmsVerifyCodeRequest request) {
        userService.preSMSLoginCheck(request);

        try {
            ClaimUser claimUser = super.authenticate(
                new SmsAuthenticationToken(request.getPhoneNumber(), request.getVerifyCode(), request.getExtraInfo())
            );

            Map<String, String> jwtTokenPair = jwtService.generateToken(claimUser);

            LoginResponse response = new LoginResponse();

            response.setTokenType(Constant.DEFAULT_TOKEN_TYPE);
            response.setAccessToken(jwtTokenPair.get(Constant.ACCESS_TOKEN));
            response.setAccessExpiresIn(jwtService.getAccessExpirationTime());
            response.setRefreshToken(jwtTokenPair.get(Constant.REFRESH_TOKEN));

            userService.smsLoginSuccessHandle(request, claimUser);

            return ResponseEntity.ok(response);
        } catch (Exception ex) {
            log.error("SMS login failed. message = {}", ex.getMessage());
            userService.smsLoginFailedHandle(request, ex);
            throw ex;
        }
    }
}
2.6.2 自定义实现AuthenticationProvider
public class UsernameAuthenticationProvider extends AbstractUsernameAuthenticationProvider {

    /**
     * The plaintext password used to perform
     * {@link PasswordEncoder#matches(CharSequence, String)} on when the user is not found
     * to avoid SEC-2056.
     */
    private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";

    private PasswordEncoder passwordEncoder;

    /**
     * The password used to perform {@link PasswordEncoder#matches(CharSequence, String)}
     * on when the user is not found to avoid SEC-2056. This is necessary, because some
     * {@link PasswordEncoder} implementations will short circuit if the password is not
     * in a valid format.
     */
    private volatile String userNotFoundEncodedPassword;

    private UserDetailsService userDetailsService;

    private UserDetailsPasswordService userDetailsPasswordService;

    public UsernameAuthenticationProvider() {
        this(PasswordEncoderFactories.createDelegatingPasswordEncoder());
    }

    /**
     * Creates a new instance using the provided {@link PasswordEncoder}
     *
     * @param passwordEncoder the {@link PasswordEncoder} to use. Cannot be null.
     * @since 6.0.3
     */
    public UsernameAuthenticationProvider(PasswordEncoder passwordEncoder) {
        setPasswordEncoder(passwordEncoder);
    }

    @Override
    @SuppressWarnings("deprecation")
    protected void additionalAuthenticationChecks(UserDetails userDetails,
                                                  UsernameAuthenticationToken authentication) throws AuthenticationException {
        if (authentication.getCredentials() == null) {
            this.logger.debug("Failed to authenticate since no credentials provided");
            throw new BadCredentialsException(this.messages
                .getMessage("act.login.bad.credentials", "Bad credentials"));
        }
        String presentedPassword = authentication.getCredentials().toString();
        if (StringUtils.isEmpty(userDetails.getPassword()) || !this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
            this.logger.debug("Failed to authenticate since password does not match stored value");
            throw new ActBadPasswordException(this.messages
                .getMessage("act.login.bad.password", "Bad Password"));
        }
    }

    @Override
    protected void doAfterPropertiesSet() {
        Assert.notNull(this.userDetailsService, "A UserDetailsService must be set");
    }

    @Override
    protected final UserDetails retrieveUser(String username, UsernameAuthenticationToken authentication)
        throws AuthenticationException {
        prepareTimingAttackProtection();
        try {
            if (!(this.getUserDetailsService() instanceof UserService userService)) {
                throw new InternalAuthenticationServiceException(
                    "UserDetailsService returned null, which is an interface contract violation");
            }

            UserDetails loadedUser = userService.findByUsername(username, authentication.getExtraInfo());
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException(
                    "UserDetailsService returned null, which is an interface contract violation");
            }

            return loadedUser;
        } catch (UsernameNotFoundException ex) {
            mitigateAgainstTimingAttack(authentication);
            throw ex;
        } catch (InternalAuthenticationServiceException ex) {
            throw ex;
        } catch (Exception ex) {
            throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
        }
    }

    @Override
    protected Authentication createSuccessAuthentication(Object principal, UsernameAuthenticationToken authentication,
                                                         UserDetails user) {
        boolean upgradeEncoding = this.userDetailsPasswordService != null
            && this.passwordEncoder.upgradeEncoding(user.getPassword());
        if (upgradeEncoding) {
            String presentedPassword = authentication.getCredentials().toString();
            String newPassword = this.passwordEncoder.encode(presentedPassword);
            user = this.userDetailsPasswordService.updatePassword(user, newPassword);
        }
        return super.createSuccessAuthentication(principal, authentication, user);
    }

    private void prepareTimingAttackProtection() {
        if (this.userNotFoundEncodedPassword == null) {
            this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD);
        }
    }

    private void mitigateAgainstTimingAttack(UsernameAuthenticationToken authentication) {
        if (authentication.getCredentials() != null) {
            String presentedPassword = authentication.getCredentials().toString();
            this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);
        }
    }

    /**
     * Sets the PasswordEncoder instance to be used to encode and validate passwords. If
     * not set, the password will be compared using
     * {@link PasswordEncoderFactories#createDelegatingPasswordEncoder()}
     *
     * @param passwordEncoder must be an instance of one of the {@code PasswordEncoder}
     *                        types.
     */
    public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
        Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
        this.passwordEncoder = passwordEncoder;
        this.userNotFoundEncodedPassword = null;
    }

    protected PasswordEncoder getPasswordEncoder() {
        return this.passwordEncoder;
    }

    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    protected UserDetailsService getUserDetailsService() {
        return this.userDetailsService;
    }

    public void setUserDetailsPasswordService(UserDetailsPasswordService userDetailsPasswordService) {
        this.userDetailsPasswordService = userDetailsPasswordService;
    }

}
public class SmsAuthenticationProvider extends AbstractSmsAuthenticationProvider {
    private UserDetailsService userDetailsService;
    private SmsVerifyCodeService smsVerifyCodeService;

    public SmsAuthenticationProvider() {
    }

    @Override
    protected void additionalAuthenticationChecks(String phoneNumber,
                                                  SmsAuthenticationToken authentication) throws AuthenticationException {
        if (authentication.getCredentials() == null) {
            this.logger.debug("Failed to authenticate since no credentials provided");
            throw new BadCredentialsException(this.messages
                .getMessage("AbstractSmsAuthenticationProvider.badCredentials", "Bad credentials"));
        }
        String inputVerifyCode = authentication.getCredentials().toString();
        String verifyCode = smsVerifyCodeService.loadVerifyCodeByPhoneNumber(phoneNumber);

        if (!verifyCode.matches(inputVerifyCode)) {
            this.logger.debug("Failed to authenticate since verify code does not match stored value");
            throw new ActBadSmsVerifyCodeException(this.messages
                .getMessage("act.login.bad.sms.verify.code", "Bad sms verify code"));
        }
    }

    @Override
    protected void doAfterPropertiesSet() {
        Assert.notNull(this.userDetailsService, "A UserDetailsService must be set");
    }

    @Override
    protected final UserDetails retrieveUser(String phoneNumber, SmsAuthenticationToken authentication)
        throws AuthenticationException {
        prepareTimingAttackProtection();
        try {

            if (!(userDetailsService instanceof UserService userService)) {
                throw new InternalAuthenticationServiceException(
                    "UserDetailsService returned null, which is an interface contract violation");
            }

            UserDetails loadedUser = userService.findByPhoneNumber(phoneNumber, authentication.getExtraInfo());
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException(
                    "UserDetailsService returned null, which is an interface contract violation");
            }
            return loadedUser;
        } catch (ActPhoneNumberNotFoundException ex) {
            mitigateAgainstTimingAttack(authentication);
            throw ex;
        } catch (InternalAuthenticationServiceException ex) {
            throw ex;
        } catch (Exception ex) {
            throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
        }
    }

    @Override
    protected Authentication createSuccessAuthentication(Object principal, SmsAuthenticationToken authentication,
                                                         UserDetails user) {
        return super.createSuccessAuthentication(principal, authentication, user);
    }

    private void prepareTimingAttackProtection() {
    }

    private void mitigateAgainstTimingAttack(SmsAuthenticationToken authentication) {
    }

    public void setSmsVerifyCodeService(SmsVerifyCodeService smsVerifyCodeService) {
        Assert.notNull(smsVerifyCodeService, "smsVerifyCodeService cannot be null");
        this.smsVerifyCodeService = smsVerifyCodeService;
    }

    protected SmsVerifyCodeService getSmsVerifyCodeService() {
        return this.smsVerifyCodeService;
    }

    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    protected UserDetailsService getUserDetailsService() {
        return this.userDetailsService;
    }

}

2.7 生成JWT token

@Service
public class JwtServiceImpl implements JwtService {
    @Autowired
    private JwtSecurityProperty jwtSecurityProperty;

    @Override
    public Claims extractAllClaims(String token) {
        return Jwts
                .parserBuilder()
                .setSigningKey(getSignInKey())
                .build()
                .parseClaimsJws(token)
                .getBody();
    }

    @Override
    public String extractSubject(String token) {
        return extractClaim(token, Claims::getSubject);
    }

    @Override
    public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = extractAllClaims(token);
        return claimsResolver.apply(claims);
    }

    @Override
    public Map<String, String> generateToken(ClaimUser claimUser) {
        Map<String, Object> accessClaims = new HashMap<>();
        accessClaims.put("id", claimUser.getId());
        accessClaims.put("username", claimUser.getUsername());
        accessClaims.put("organizationId", claimUser.getOrganizationId());
        accessClaims.put("authorities", claimUser.getAuthorities());

        Map<String, Object> refreshClaims = new HashMap<>();
        refreshClaims.put("id", claimUser.getId());

        return generateTokenPair(accessClaims, refreshClaims,
                claimUser.getUsername(),
                String.valueOf(claimUser.getId()),
                System.currentTimeMillis(),
                jwtSecurityProperty.getAccessExpirationTime(),
                jwtSecurityProperty.getRefreshExpirationTime());
    }

    public Map<String, String> generateTokenPair(Map<String, Object> accessClaims,
                                                 Map<String, Object> refreshClaims,
                                                 String accessTokenSubject,
                                                 String refreshTokenSubject,
                                                 long issuedAt,
                                                 long accessTokenExpiration,
                                                 long refreshTokenExpiration) {

        Map<String, String> tokenPair = new HashMap<>();

        String accessToken = buildToken(accessClaims, accessTokenSubject, issuedAt, accessTokenExpiration);
        tokenPair.put(Constant.ACCESS_TOKEN, accessToken);

        String refreshToken = buildToken(refreshClaims, refreshTokenSubject, issuedAt, refreshTokenExpiration);
        tokenPair.put(Constant.REFRESH_TOKEN, refreshToken);

        return tokenPair;
    }

    public long getAccessExpirationTime() {
        return jwtSecurityProperty.getAccessExpirationTime();
    }

    public long getRefreshExpirationTime() {
        return jwtSecurityProperty.getRefreshExpirationTime();
    }

    private String buildToken(
            Map<String, Object> extraClaims,
            String subject,
            long issuedAt,
            long expiration
    ) {
        return Jwts
                .builder()
                .setClaims(extraClaims)
                .setSubject(subject)
                .setIssuedAt(new Date(issuedAt + expiration))
                .setExpiration(new Date(issuedAt + expiration))
                .signWith(getSignInKey(), SignatureAlgorithm.HS256)
                .compact();
    }

    public boolean isTokenValid(String token, String subject) {
        return (extractSubject(token).equals(subject)) && !isTokenExpired(token);
    }

    private boolean isTokenExpired(String token) {
        return extractExpiration(token).before(new Date());
    }

    private Date extractExpiration(String token) {
        return
                extractClaim(token, Claims::getExpiration);
    }

    private Key getSignInKey() {
        byte[] keyBytes = Decoders.BASE64.decode(jwtSecurityProperty.getSecretKey());
        return Keys.hmacShaKeyFor(keyBytes);
    }
}

2.8 访问服务解析JWT token

2.8.1 JWT token 解析过滤器
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final Collection<RequestMatcher> requestIgnoreRequestMatchers;
    private final HandlerExceptionResolver handlerExceptionResolver;

    private final JwtService jwtService;

    public JwtAuthenticationFilter(
        Collection<RequestMatcher> requestIgnoreRequestMatchers, JwtService jwtService,
        HandlerExceptionResolver handlerExceptionResolver
    ) {
        this.requestIgnoreRequestMatchers = requestIgnoreRequestMatchers;
        this.jwtService = jwtService;

        this.handlerExceptionResolver = handlerExceptionResolver;
    }

    @Override
    protected void doFilterInternal(
        @NonNull HttpServletRequest request,
        @NonNull HttpServletResponse response,
        @NonNull FilterChain filterChain
    ) throws ServletException, IOException {
        final String authHeader = request.getHeader("Authorization");

        if (authHeader == null || !authHeader.startsWith("Bearer ") || isIgnoreRequests(request)) {
            filterChain.doFilter(request, response);
            return;
        }

        try {
            final String jwt = authHeader.substring(7);
            final var claims = jwtService.extractAllClaims(jwt);
            final String username = claims.getSubject();

            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            if (username != null && authentication == null) {

                if (jwtService.isTokenValid(jwt, username)) {
                    ClaimUser claimUser = convert(claims);

                    Collection<? extends GrantedAuthority> authorities
                        = AuthorityUtils.createAuthorityList(claimUser.getAuthorities().toArray(new String[0]));

                    UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
                        claimUser,
                        null,
                        authorities
                    );

                    authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authToken);
                }
            }

            filterChain.doFilter(request, response);
        } catch (Exception exception) {
            handlerExceptionResolver.resolveException(request, response, null, exception);
        }
    }

    private ClaimUser convert(Claims claims) {
        ClaimUser claimUser = new ClaimUser();
        claimUser.setId(claims.get("id", Long.class));
        claimUser.setOrganizationId(claims.get("organizationId", Long.class));
        claimUser.setUsername(claims.getSubject());

        var data = claims.get("authorities", Collection.class);
        List<String> authorities = new ArrayList<>();
        for (Object obj : data) {
            authorities.add(obj.toString());
        }
        claimUser.setAuthorities(authorities);

        return claimUser;

    }

    private boolean isIgnoreRequests(HttpServletRequest request) {

        for (RequestMatcher requestMatcher : this.requestIgnoreRequestMatchers) {
            if (requestMatcher.matches(request)) {
                return true;
            }
        }

        return false;
    }
}
2.8.2 配置JWT token过滤器
@Slf4j
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
@Configuration(proxyBeanMethods = false)
public class AuthServerSecurityConfig {
    private final JwtAuthenticationFilter jwtAuthenticationFilter;

    public AuthServerSecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter
    ) {
        this.jwtAuthenticationFilter = jwtAuthenticationFilter;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.exceptionHandling(exceptionHandlingConfig -> exceptionHandlingConfig
                        .authenticationEntryPoint(new UnauthorizedEntryPoint()))
                .csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests((authorize) -> authorize
                        .anyRequest().authenticated()
                )
                .sessionManagement((sessionManagement) -> sessionManagement
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                )
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

2.9 将core中的登录相关bean注入登录Service

2.9.1 创建注解,导入core config
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({AuthServerAppConfig.class})
public @interface EnableActsAuthServer {
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({SmsAuthServerController.class})
public @interface EnableActsAuthSmsLogin {
}
2.9.2 创建 config
@Configuration
@Import({AuthResourceSecurityConfig.class,
        WebSecurityCustomizerConfig.class,
        PasswordEncoderConfig.class,
        WebMvcConfig.class,
        GrpcAuthConfig.class,
        RedisCacheConfig.class,
        ModelMapperConfig.class,
        MessageSourceConfig.class,
        OpenAPISecurityConfig.class,
        ObservedAspectConfig.class,
        GrpcZipkinConfig.class})
@ComponentScans(value = {
        @ComponentScan(value = "com.core.properties"),
        @ComponentScan(value = "com.core.filter"),
        @ComponentScan(value = "com.core.component"),
        @ComponentScan(value = "com.core.exceptions"),
        @ComponentScan(value = "com.core.utils"),
        @ComponentScan(value = "com.core.controller", excludeFilters = {
                @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = {
                       SmsAuthServerController.class
                })
        }),
        @ComponentScan(value = "com.core.service")
})
public class AuthServerAppConfig {
    private final UserService userService;

    private final PasswordEncoder passwordEncoder;

    @Autowired(required = false)
    private SmsVerifyCodeService smsVerifyCodeService;
    private final AuthenticationManagerBuilder authenticationManagerBuilder;

    public AuthServerAppConfig(UserService userService, PasswordEncoder passwordEncoder, AuthenticationManagerBuilder authenticationManagerBuilder) {
        this.userService = userService;
        this.passwordEncoder = passwordEncoder;
        this.authenticationManagerBuilder = authenticationManagerBuilder;
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        authenticationManagerBuilder.authenticationProvider(authenticationProvider());

        AuthenticationProvider smsAuthenticationProvider = smsAuthenticationProvider();
        if (smsAuthenticationProvider != null) {
            authenticationManagerBuilder.authenticationProvider(smsAuthenticationProvider());
        }

        return config.getAuthenticationManager();
    }

    @Bean
    AuthenticationProvider authenticationProvider() {
        UsernameAuthenticationProvider authProvider = new UsernameAuthenticationProvider();

        authProvider.setUserDetailsService(userService);
        authProvider.setPasswordEncoder(passwordEncoder);

        return authProvider;
    }

    private AuthenticationProvider smsAuthenticationProvider() {
        if (smsVerifyCodeService == null) {
            return null;
        }

        SmsAuthenticationProvider authProvider = new SmsAuthenticationProvider();
        authProvider.setUserDetailsService(userService);
        authProvider.setSmsVerifyCodeService(smsVerifyCodeService);

        return authProvider;
    }
}
2.9.3 启动类添加注解
@EnableActsAuthSmsLogin
public class PlatformApplication {
	public static void main(String[] args) {
		SpringApplication.run(PlatformApplication.class, args);
	}
}
@EnableActsAuthServer
public class AdminApplication {
	 public static void main(String[] args) {
        SpringApplication.run(AdminApplication.class, args);
    }
}

致谢

感谢领导的指导

  • 30
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Spring Boot项目中集成Spring Security非常简单,只需要在项目中增加Spring Boot Security的依赖即可。下面是一个基础的Spring Boot Security登录验证的示例: 1.在pom.xml文件中添加Spring Boot Security依赖: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> ``` 2.创建一个WebSecurityConfig类,用于配置Spring Security: ```java @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/", "/home").permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") .permitAll() .and() .logout() .permitAll(); } @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("user").password("password").roles("USER"); } } ``` 3.在上面的代码中,我们配置了一个基本的登录验证,只有经过身份验证的用户才能访问应用程序的其他部分。我们还定义了一个用户“user”,密码为“password”,并将其分配给“USER”角色。 4.在应用程序中,我们还需要一个登录页面。在templates文件夹中创建一个名为“login.html”的文件,添加以下内容: ```html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8" /> <title>Login</title> </head> <body> <h1>Login</h1> <form th:action="@{/login}" method="post"> <div><label>Username: <input type="text" name="username" /></label></div> <div><label>Password: <input type="password" name="password" /></label></div> <<div><input type="submit" value="Sign In" /></div> </form> </body> </html> ``` 5.最后,在应用程序的控制器中添加一个映射到登录页面的请求处理程序: ```java @Controller public class HomeController { @RequestMapping(value = {"/", "/home"}) public String home() { return "home"; } @RequestMapping(value = "/login") public String login() { return "login"; } } ``` 这样,我们就完成了一个基本的Spring Boot Security登录验证的配置。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

二进制诗人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值