Spring Security Oauth2 client支持GitHub第三方登录(六)

说明

spring-security-oauth 这个项目不赞成使用了。oauth2已经由Spring Security提供服务。Spring Security没有提供对认证服务器的支持,需要 spring-authorization-server 去支持。
https://spring.io/blog/2020/04/15/announcing-the-spring-authorization-server

The Spring Security OAuth project is deprecated. The latest OAuth 2.0 support is provided by Spring Security. See the OAuth 2.0 Migration Guide for further details.

Since Spring Security doesn’t provide Authorization Server support, migrating a Spring Security OAuth Authorization Server see https://spring.io/blog/2020/04/15/announcing-the-spring-authorization-server

集成github第三方登录

添加依赖 build.gradle

implementation "org.springframework.boot:spring-boot-starter-oauth2-client"
implementation 'org.springframework.boot:spring-boot-starter-web'

配置文件 application.yml

spring:
  security:
    oauth2:
      client:
        registration:
          github:
            client-id: xxx
            client-secret: xxx
            authorization-grant-type: authorization_code
            # /login/oauth2/code/ 固定格式
            redirect-uri: "http://localhost:8080/login/oauth2/code/github"
            # 只有一个权限就是user
            scope: user
        provider:
          github:
            authorization-uri: https://github.com/login/oauth/authorize
            token-uri: https://github.com/login/oauth/access_token

前往OAuth Apps配置github client信息

配置类 OAuth2ClientSecurityConfig.java

@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, jsr250Enabled=true, prePostEnabled = true)
@Slf4j
public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests(a -> a
                        .antMatchers("/public", "/error", "/webjars/**").permitAll()
                        .antMatchers("/admin").hasAnyRole("admin")
                        .anyRequest().authenticated()
                )
                .exceptionHandling(e -> e
                                .authenticationEntryPoint((request, response, e1) -> {
                                    log.error("认证异常:{}", e1.getMessage());
                                    response.sendRedirect("oauth2/authorization/github");
                                })
                )
                .oauth2Login().successHandler(((request, response, authentication) -> {
                    log.error("认证成功");
            OAuth2AuthenticationToken oAuth2AuthenticationToken = (OAuth2AuthenticationToken)authentication;
                    log.info("{} 在github上的信息是:{}", oAuth2AuthenticationToken.getPrincipal().getName(),
                            oAuth2AuthenticationToken);
                }));
    }
}
  • /public 是不需要认证就可以访问的
  • /admin 是需要admin角色才能访问的
  • /user 是需要user角色才能访问的
  • /index 是需要认证的

@EnableGlobalMethodSecurity 注解允许在方法级别使用@PreAuthorize(“hasAnyAuthority(‘ROLE_USER, SCOPE_user’)”)

测试接口

@RestController
public class ResourceController {

    /**
     * 通过token,可以访问github上的其他资源
     * curl -X GET \
     *   https://api.github.com/user \
     *   -H 'Authorization: Bearer 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' \
     * @param authorizedClient
     * @return
     */
    @GetMapping(value = {"index", "/"})
    public String index(@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient) {
        return "index token = " + authorizedClient.getAccessToken().getTokenValue();
    }

    /**
     * 403没有访问权限
     * @return
     */
    @GetMapping(value = "admin")
    public String admin() {
        return "admin";
    }

    @PreAuthorize("hasAnyAuthority('ROLE_USER, SCOPE_user')")
    @GetMapping(value = "user")
    public String user() {
        return "user";
    }

    /**
     * 不需要认证就能访问
     * @return
     */
    @GetMapping(value = "public")
    public String publicFun() {
        return "public";
    }
}

源码分析

  • OAuth2AuthorizationRequestRedirectFilter.java
private void sendRedirectForAuthorization(HttpServletRequest request, HttpServletResponse response, OAuth2AuthorizationRequest authorizationRequest) throws IOException {
    if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(authorizationRequest.getGrantType())) {
        this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response);
    }

    this.authorizationRedirectStrategy.sendRedirect(request, response, authorizationRequest.getAuthorizationRequestUri());
}

拦截器组装数据后跳转以下URL

https://github.com/login/oauth/authorize?response_type=code&client_id=&redirect_uri=http://localhost:8080/login/oauth2/code/github
  • OAuth2LoginAuthenticationFilter.java
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
    MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
    if (!OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) {
        OAuth2Error oauth2Error = new OAuth2Error("invalid_request");
        throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
    } else {
        OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository.removeAuthorizationRequest(request, response);
        if (authorizationRequest == null) {
            OAuth2Error oauth2Error = new OAuth2Error("authorization_request_not_found");
            throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
        } else {
            String registrationId = (String)authorizationRequest.getAttribute("registration_id");
            ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
            if (clientRegistration == null) {
                OAuth2Error oauth2Error = new OAuth2Error("client_registration_not_found", "Client Registration not found with Id: " + registrationId, (String)null);
                throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
            } else {
                String redirectUri = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request)).replaceQuery((String)null).build().toUriString();
                OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponseUtils.convert(params, redirectUri);
                Object authenticationDetails = this.authenticationDetailsSource.buildDetails(request);
                OAuth2LoginAuthenticationToken authenticationRequest = new OAuth2LoginAuthenticationToken(clientRegistration, new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse));
                authenticationRequest.setDetails(authenticationDetails);
                // 委托Provider认证,认证成功后,返回OAuth2LoginAuthenticationToken
                OAuth2LoginAuthenticationToken authenticationResult = (OAuth2LoginAuthenticationToken)this.getAuthenticationManager().authenticate(authenticationRequest);
                OAuth2AuthenticationToken oauth2Authentication = new OAuth2AuthenticationToken(authenticationResult.getPrincipal(), authenticationResult.getAuthorities(), authenticationResult.getClientRegistration().getRegistrationId());
                oauth2Authentication.setDetails(authenticationDetails);
                OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(authenticationResult.getClientRegistration(), oauth2Authentication.getName(), authenticationResult.getAccessToken(), authenticationResult.getRefreshToken());
                this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, oauth2Authentication, request, response);
                return oauth2Authentication;
            }
        }
    }
}

OAuth2LoginAuthenticationFilter 的作用很简单,就是响应授权服务器的回调地址(/login/oauth2/code/github),核心之处在于OAuth2LoginAuthenticationProviderOAuth2LoginAuthenticationToken 的认证。

OAuth2LoginAuthenticationProvider.java

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    OAuth2LoginAuthenticationToken loginAuthenticationToken = (OAuth2LoginAuthenticationToken)authentication;
    if (loginAuthenticationToken.getAuthorizationExchange().getAuthorizationRequest().getScopes().contains("openid")) {
        return null;
    } else {
        OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthenticationToken;
        try {
            authorizationCodeAuthenticationToken = (OAuth2AuthorizationCodeAuthenticationToken)this.authorizationCodeAuthenticationProvider.authenticate(new OAuth2AuthorizationCodeAuthenticationToken(loginAuthenticationToken.getClientRegistration(), loginAuthenticationToken.getAuthorizationExchange()));
        } catch (OAuth2AuthorizationException var9) {
            OAuth2Error oauth2Error = var9.getError();
            throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
        }

        OAuth2AccessToken accessToken = authorizationCodeAuthenticationToken.getAccessToken();
        Map<String, Object> additionalParameters = authorizationCodeAuthenticationToken.getAdditionalParameters();
        OAuth2User oauth2User = this.userService.loadUser(new OAuth2UserRequest(loginAuthenticationToken.getClientRegistration(), accessToken, additionalParameters));
        Collection<? extends GrantedAuthority> mappedAuthorities = this.authoritiesMapper.mapAuthorities(oauth2User.getAuthorities());
        OAuth2LoginAuthenticationToken authenticationResult = new OAuth2LoginAuthenticationToken(loginAuthenticationToken.getClientRegistration(), loginAuthenticationToken.getAuthorizationExchange(), oauth2User, mappedAuthorities, accessToken, authorizationCodeAuthenticationToken.getRefreshToken());
        authenticationResult.setDetails(loginAuthenticationToken.getDetails());
        return authenticationResult;
    }
}

OAuth2LoginAuthenticationToken.java

public OAuth2LoginAuthenticationToken(ClientRegistration clientRegistration, OAuth2AuthorizationExchange authorizationExchange, OAuth2User principal, Collection<? extends GrantedAuthority> authorities, OAuth2AccessToken accessToken, @Nullable OAuth2RefreshToken refreshToken) {
    super(authorities);
    Assert.notNull(clientRegistration, "clientRegistration cannot be null");
    Assert.notNull(authorizationExchange, "authorizationExchange cannot be null");
    Assert.notNull(principal, "principal cannot be null");
    Assert.notNull(accessToken, "accessToken cannot be null");
    this.clientRegistration = clientRegistration;
    this.authorizationExchange = authorizationExchange;
    this.principal = principal;
    this.accessToken = accessToken;
    this.refreshToken = refreshToken;
    this.setAuthenticated(true);
}

OAuth2LoginAuthenticationToken 有principal accessToken refreshToken等信息

参考阅读:

https://www.zyc.red/Spring/Security/OAuth2/OAuth2-Client/
Github:https://github.com/jkxyx205/spring-security-learn/tree/master/spring-security-oauth2-client

Spring Security短信登录实现(五)

组件Filter、Provider、AuthenticationToken

SmsCodeAuthenticationFilter.java

public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

        private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/sms/login",
                "POST");

        public SmsCodeAuthenticationFilter(AuthenticationManager authenticationManager) {
            super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
            setAuthenticationManager(authenticationManager);
        }

        @Override
        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
            String mobile = request.getParameter("mobile");
            String code = request.getParameter("code");

            SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile, code);
            setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        }

        protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
            authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
        }
}

SmsCodeAuthenticationToken.java

public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {

        private String mobile;

        private String code;

        public SmsCodeAuthenticationToken(String mobile, String code) {
            super(null);
            this.mobile = mobile;
            this.code = code;
            setAuthenticated(false);
        }

        public SmsCodeAuthenticationToken(String mobile, String code,
                                                   Collection<? extends GrantedAuthority> authorities) {
            super(authorities);
            this.mobile = mobile;
            this.code = code;
            super.setAuthenticated(true); // must use super, as we override
        }

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

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

SmsCodeAuthenticationProvider.java

@AllArgsConstructor
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String mobile = (String)authentication.getPrincipal();
        String code = (String) authentication.getCredentials();
        // TODO 验证码
        validate(mobile, code);

        UserDetails userDetails = userDetailsService.loadUserByUsername(mobile);
        SmsCodeAuthenticationToken successAuthentication = new SmsCodeAuthenticationToken(userDetails.getUsername(),
                userDetails.getPassword(), userDetails.getAuthorities());

        return successAuthentication;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication == SmsCodeAuthenticationToken.class;
    }

    private void validate(String mobile, String code) {
        if (!"888888".equals(code)) {
            throw new BadCredentialsException("验证码不正确!");
        }
    }

}

添加到配置

  • 添加Provider
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.authenticationProvider(new SmsCodeAuthenticationProvider(username -> new User("jkxyx205", "11", AuthorityUtils.commaSeparatedStringToAuthorityList("USER"))));
}
  • 添加Filter
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.addFilterBefore(new SmsCodeAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);
}

测试页面

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
>
<head>
    <meta charset="UTF-8">
    <title>自定义登录</title>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.0.2/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div style="margin: 40px auto; width: 320px;">
        <th:block th:if="${session.SPRING_SECURITY_LAST_EXCEPTION != null}">
            <div th:text="${session.SPRING_SECURITY_LAST_EXCEPTION.message}"></div>
        </th:block>
        <form action="/sms/login" method="post">
            <div class="mb-3 row">
                <label for="mobile" class="visually-hidden">用户名</label>
                <input type="text" class="form-control" id="mobile" name="mobile" value="18898987724">
            </div>
            <div class="mb-3 row">
                <label for="code" class="visually-hidden">手机验证码</label>
                <input type="text" class="form-control" id="code" name="code" value="888888">
            </div>
            <div class="mb-3 row">
                <button type="submit" class="btn btn-primary mb-3">登录</button>
            </div>
        </form>
    </div>
</body>
</html>

启动日志

2021-10-10 12:41:07.188  INFO 54574 --- [           main] o.s.s.web.DefaultSecurityFilterChain     : Will secure any request with [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@4694f434, org.springframework.security.web.context.SecurityContextPersistenceFilter@ceddaf8, org.springframework.security.web.header.HeaderWriterFilter@5f174dd2, org.springframework.security.web.authentication.logout.LogoutFilter@608b1fd2, com.rick.security.authentication.sms.SmsCodeAuthenticationFilter@56928e17, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@6dc1dc69, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@4833eff3, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@1db87583, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@6993c8df, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@dd2856e, org.springframework.security.web.session.SessionManagementFilter@45aca496, org.springframework.security.web.access.ExceptionTranslationFilter@10a98392, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@70c69586]

SmsCodeAuthenticationFilter 已经添加到了 DefaultSecurityFilterChain
Github:https://github.com/jkxyx205/spring-security-learn/tree/smscode-filter

Spring Security表单认证流程分析(四)

Spring Security过滤器

HttpSecurity.java#performBuild 获取 DefaultSecurityFilterChain

http://xhope.top/wp-content/uploads/2021/10/s1.png

0 = {WebAsyncManagerIntegrationFilter@5192}
这个过滤器用于集成SecurityContext到Spring异步执行机制的WebAsyncManager 中。如果想要与spring集成,就必须要使用此过滤器链。

1 = {SecurityContextPersistenceFilter@6142} 
主要是使用SecurityContextRepository在session中保存或更新一个SecurityContext域对象(相当于一个容器),并将SecurityContext给以后的过滤器使用,来为后续filter建立所需的上下文。SecurityContext中存储了当前用户的认证以及权限信息。 其他的过滤器都需要依赖于它。在 Spring Security 中,虽然安全上下文信息被存储于 Session 中,但我们在实际使用中不应该直接操作 Session,而应当使用 SecurityContextHolder。

2 = {HeaderWriterFilter@6125} 
用于将头信息加入响应中。

3 = {LogoutFilter@6163}
匹配URL为/logout的请求,实现用户退出,清除认证信息。

4 = {UsernamePasswordAuthenticationFilter@6199}
认证操作全靠这个过滤器,默认匹配URL为/login且必须为POST请求。

5 = {BasicAuthenticationFilter@6250} 
此过滤器会自动解析HTTP请求中头部名字为Authentication,且以Basic开头的头信息。

6 = {RequestCacheAwareFilter@6146} 
通过HttpSessionRequestCache内部维护了一个RequestCache,用于缓存HttpServletRequest。

7 = {SecurityContextHolderAwareRequestFilter@6156}
针对ServletRequest进行了一次包装,使得request具有更加丰富的API。

8 = {AnonymousAuthenticationFilter@6150} 
当SecurityContextHolder中认证信息为空,则会创建一个匿名用户存入到SecurityContextHolder中。
spring security为了兼容未登录的访问,也走了一套认证流程,只不过是一个匿名的身份(游客)

9 = {SessionManagementFilter@6137} 
SecurityContextRepository限制同一用户开启多个会话的数量

10 = {ExceptionTranslationFilter@6113} 
异常转换过滤器位于整个springSecurityFilterChain的后方,用来转换整个链路中出现的异常

11 = {FilterSecurityInterceptor@6194}
获取所配置资源访问的授权信息,根据SecurityContextHolder中存储的用户信息来决定其是否有权限

认证流程

POST /login

  1. SecurityContextPersistenceFilter:从SecurityContextRepository获取认证信息,放入上下文中。如果未认证,createEmptyContext。
  2. UsernamePasswordAuthenticationFilter:
    • 是否路径是/login,请求是POST
    • 获取用户名密码,包装成对象 UsernamePasswordAuthenticationTokenProviderManager 进行验证
    • ProviderManager 使用 DaoAuthenticationProvider 进行处理。因为它supports UsernamePasswordAuthenticationToken。
    • 根据username获取UserDetails
    • 检查账号有没有锁定,过期,可用等
    • 检查用户名密码是否匹配
    • 检查密码是否过期
    • 验证成功后,返回成功的Authentication。放到SecurityContextHolder上下文中。记住我,successHandler。
    • 验证失败后,AbstractAuthenticationProcessingFilter捕获异常,处理failureHandler
  3. AnonymousAuthenticationFilter:上下文中没有认证信息,创建匿名认证
  4. FilterSecurityInterceptor: this.accessDecisionManager.decide(authenticated, object, attributes) 权限进行投票(通过spring-expression表达式去判断)。
    默认投票实现 AffirmativeBased 的逻辑:

    a.只要有AccessDecisionVoter的投票为ACCESS_GRANTED则同意用户进行访问;

    b.如果全部弃权也表示通过;

    c.如果没有一个人投赞成票,但是有人投反对票,则将抛出AccessDeniedException。

POST /public

  1. SecurityContextPersistenceFilter:从SecurityContextRepository获取认证信息,放入上下文中。如果未认证,createEmptyContext。
  2. AnonymousAuthenticationFilter
  3. FilterSecurityInterceptor: 进行投票

POST /index

  1. SecurityContextPersistenceFilter:从SecurityContextRepository获取认证信息,放入上下文中。如果未认证,createEmptyContext。
  2. AnonymousAuthenticationFilter
  3. FilterSecurityInterceptor: 进行投票

POST /admin

  1. SecurityContextPersistenceFilter:从SecurityContextRepository获取认证信息,放入上下文中。如果未认证,createEmptyContext。
  2. AnonymousAuthenticationFilter
  3. FilterSecurityInterceptor: 进行投票

参考:

Spring Security自定义表单登录(三)

添加thymeleaf支持

application.yml

spring:
  thymeleaf:
    prefix: classpath:/templates/
    suffix: .html
    encoding: UTF-8

添加依赖

implementation "org.springframework.boot:spring-boot-starter-thymeleaf"
implementation "org.thymeleaf.extras:thymeleaf-extras-springsecurity5"

新增页面login.html

新增页面 /templates/login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>自定义登录</title>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.0.2/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div style="margin: 40px auto; width: 320px;">
        <form action="/login" method="post">
            <div class="mb-3 row">
                <label for="username" class="visually-hidden">用户名</label>
                <input type="text" class="form-control" id="username" name="username" placeholder="请输入用户名">
            </div>
            <div class="mb-3 row">
                <label for="password" class="visually-hidden">密码</label>
                <input type="password" class="form-control" id="password" name="password" placeholder="请输入密码">
            </div>
            <div class="mb-3 row">
                <button type="submit" class="btn btn-primary mb-3">登录</button>
            </div>
        </form>
    </div>
</body>
</html>

控制器跳转
MvcConfig.java

@Configuration
public class MvcConfig implements WebMvcConfigurer {

   @Override
   public void addViewControllers(ViewControllerRegistry registry) {
       registry.addViewController("/login");
   }

}

添加配置

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests((requests) -> requests.antMatchers("/public").permitAll()
            .antMatchers(HttpMethod.GET, "/login").permitAll()
            .antMatchers("/admin").hasAnyRole("ADMIN")).cors().disable();

    http.formLogin()
            // 登录页面的路径
            .loginPage("/login");
    http.httpBasic();
}

注意:

  • csrf().disable(); csrf会拦截POST请求,需要禁用
  • .loginPage(“/login”); 设置登录页面为/login
  • antMatchers(HttpMethod.GET, “/login”).permitAll(); 登录页面不需要认证

配置认证成功失败处理器

final AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
final AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler("/login?error");

http.formLogin()
        .successHandler((request, response, authentication) -> {
            log.info("{} 登录成功", authentication.getName());
            // 跳转到认证前访问到地址(默认就是这个处理器)
            successHandler.onAuthenticationSuccess(request, response, authentication);
        })
        .failureHandler((request, response, exception) -> {
            log.error("登录失败 {}", exception.getMessage());
            // 会将exception放入session中,页面可以通过session获取异常
            failureHandler.onAuthenticationFailure(request, response, exception);
//                    response.sendRedirect("/login?error=" + exception.getMessage());
        })
        });

login.html 显示错误信息

<th:block th:if="${session.SPRING_SECURITY_LAST_EXCEPTION != null}">
    <div th:text="${session.SPRING_SECURITY_LAST_EXCEPTION.message}"></div>
</th:block>

配置退出登录

http.formLogin()
    .logout()
    .logoutSuccessHandler((request, response, authentication) -> {
        log.info("{} 退出登录", authentication.getName());
        response.sendRedirect("/login");
    });
GET http://localhost:8080/logout

配置异常处理器

final LoginUrlAuthenticationEntryPoint loginUrlAuthenticationEntryPoint = new LoginUrlAuthenticationEntryPoint("/login");

http.authorizeRequests((requests) -> requests.antMatchers("/public").permitAll()
        .antMatchers(HttpMethod.GET, "/login").permitAll()
        .antMatchers("/admin").hasAnyRole("ADMIN")
        .anyRequest().authenticated())
        .exceptionHandling().authenticationEntryPoint((request, response, authException) -> {
        log.warn("认证异常");
        // 默认就是LoginUrlAuthenticationEntryPoint
        loginUrlAuthenticationEntryPoint.commence(request, response, authException);
})
        .and()
        .exceptionHandling().accessDeniedHandler((request, response, accessDeniedException) -> {
    // 403 Forbidden 没有授权,执行到此处
    log.warn("没有权限");
    HttpServletResponseUtils.write(response, "text/plain", "403 Forbidden");
});
  • authenticationEntryPoint:它在用户请求处理过程中遇到认证异常时
  • accessDeniedHandler: 没有访问权限

ExceptionTranslationFilter.java

private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response,
        FilterChain chain, RuntimeException exception) throws IOException, ServletException {
    if (exception instanceof AuthenticationException) {
        handleAuthenticationException(request, response, chain, (AuthenticationException) exception);
    }
    else if (exception instanceof AccessDeniedException) {
        handleAccessDeniedException(request, response, chain, (AccessDeniedException) exception);
    }
}

Github:https://github.com/jkxyx205/spring-security-learn/tree/form-configurer

Spring Security简单配置(二)

自定义密码策略

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

自定义用户信息并授权

配置文件

通过 autoconfigure自动配置类

application.yml

spring:
  security:
    user:
      name: rick
      password: $2a$10$Qs4BkX/ljq09QuYcE6GwBewe9aKIW9NlXvRFyqDurmZcyGcFzDXIq # 123456编码后的密码
      roles:
        - ADMIN

编程式

  • 注册 UserDetailsService
  • AuthenticationManagerBuilder

SecurityConfig.java

@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, jsr250Enabled=true, prePostEnabled = true)// 控制权限注解 配合 @Secured({"ROLE_ADMIN","ROLE_USER2"})使用
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;


    /**
     * 方式一:自定义用户信息并授权
     * @return
     */
    @Bean
    @Override
    public UserDetailsService userDetailsService() {
        //Admin Role
        UserDetails theUser = User.withUsername("rick")
                .password(passwordEncoder.encode("123456"))
                .roles("ADMIN").build();

        //User Role
        UserDetails theManager = User.withUsername("john")
                .password(passwordEncoder.encode("123456"))
                .roles("USER").build();


        InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();

        userDetailsManager.createUser(theUser);
        userDetailsManager.createUser(theManager);
        return userDetailsManager;
    }


    /**
     * 方式二:自定义用户信息并授权
     * @return
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("rick")
                .password(passwordEncoder.encode("123456"))
                .roles("ADMIN")
                .and()
                .withUser("john")
                .password(passwordEncoder.encode("123456"))
                .roles("USER");

    }
}

不能再使用默认的user和生成的密码登录了。必须使用rick/123456登录

授权请求

IndexController.java

@RestController
public class IndexController {

    @GetMapping(value = {"index", "/"})
    public String index() {
        return "index";
    }

    @GetMapping("public")
    public String publicFun() {
        return "public";
    }

    @GetMapping("admin")
    public String admin() {
        return "admin";
    }

    @PreAuthorize("hasAnyRole('USER')")
    @GetMapping("user")
    public String user() {
        return "user";
    }
}

SecurityConfig.java

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests((requests) -> requests.antMatchers("/public").permitAll()
            .antMatchers("/admin").hasAnyRole("ADMIN")
            .anyRequest().authenticated())
            .exceptionHandling().accessDeniedHandler((request, response, accessDeniedException) -> {
                // 403 Forbidden 没有授权,执行到此处
        HttpServletResponseUtils.write(response, "text/plain", "403 Forbidden");
    });
    http.formLogin();
    http.httpBasic();
}
  • /public 不需要登录就能访问
  • /index 认证后允许访问
  • /admin 认证后,角色ADMIN才允许访问

Github:https://github.com/jkxyx205/spring-security-learn/tree/simple-configure