Spring Security OAuth2.0自定义登录页面 + JWT Token配置

上一篇文章主要讲解Spring Security基本原理,本文主要讲如何配置使用Spring Security,包括

  1. OAuth2.0认证配置
  2. 自定义登录页面实现与配置
  3. JWT token生成配置

基本介绍

pom引用

需要加入spring security和spring security oauth2的依赖


            org.springframework.boot
            spring-boot-starter-security
        
        
            org.springframework.cloud
            spring-cloud-starter-oauth2
        

OAuth2.0认证配置

WebSecurityConfig配置

  1. 首先进行登录页面相关配置
  • 配置自定义登录页面为index.html,后端用户名及密码验证为"/login"
  • 因为为自定义登录页面,配置"/index.html", "/login", "/resources/**", "/static/"不需要认证,其它请求需要认证。否则登录页面无法打开
  • 配置退出登录时,删除session和cookie,并自定义退出成功后的handler
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
    protected void configure(HttpSecurity http) throws Exception {
        // 默认支持./login实现authorization_code认证
        http
            .formLogin().loginPage("/index.html").loginProcessingUrl("/login")
            .and()
            .authorizeRequests()
            .antMatchers("/index.html", "/login", "/resources/**", "/static/**").permitAll()
            .anyRequest() // 任何请求
            .authenticated()// 都需要身份认证
            .and()
            .logout().invalidateHttpSession(true).deleteCookies("JSESSIONID").logoutSuccessHandler(customLogoutSuccessHandler).permitAll()
            .and()
            .csrf().disable();
    }
  1. 加入自定义的UsernamePasswordAuthenticationProvider,用于实现用户名和密码认证
@Override
    public void configure(AuthenticationManagerBuilder auth) {
        // 定义认证的provider用于实现用户名和密码认证
        auth.authenticationProvider(new UsernamePasswordAuthenticationProvider(usernamePasswordUserDetailService));
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
  1. 实现UsernamePasswordAuthenticationProvider实现authenticate和supports方法用于用户认证
public class UsernamePasswordAuthenticationProvider implements AuthenticationProvider {
    // 自定义实现的user detail,使用了用户名和密码验证,用户信息的获取
    private UsernamePasswordUserDetailService userDetailService;

    public UsernamePasswordAuthenticationProvider(UsernamePasswordUserDetailService userDetailService) {
        this.userDetailService = userDetailService;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        String password = authentication.getCredentials().toString();

        // 验证用户名和密码
        if (userDetailService.verifyCredential(username, password)) {

            List authorities = new ArrayList<>();
            authorities.add(new SimpleGrantedAuthority("user"));
            UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password, authorities);
            //获取用户信息
            token.setDetails(userDetailService.getUserDetail(username));
            return token;
        }
        return null;
    }

    @Override
    public boolean supports(Class aClass) {
        // 用户名和密码登录时,使用该provider认证
        return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(aClass));
    }
}
  1. 将自定义的登录页面编译后拷贝到resource/public文件下,登录页面为index.html。可以自行实现自己版本的登录页面,也可以参考文末awesome-admin中login-ui源码链接。


    登录页面

JWT Token配置

Oauth2AuthorizationConfig配置

  1. 配置Oauth2 Client,本文通过ClientDetailsService实现Oauth2 Client.
@Configuration
@EnableAuthorizationServer
public class Oauth2AuthorizationConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    ClientDetailsService clientDetailsService;

    ....

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(clientDetailsService);
    }
}

ClientDetailsService继承ClientDetailsService,正常应当从数据库中读取,为了演示方便,本文直接在代码中设置。客户端信息包括appId, appSecret, grantTypes, accessExpireTime, refreshExpireTime, redirectUrl, scopes等OAuth2协议中Client信息。

@Service
@Primary
public class ClientDetailsServiceImpl implements ClientDetailsService {
    @Override
    public ClientDetails loadClientByClientId(String s) {

        if ("weixin".equals(s) || "app".equals(s) || "mini_app".equals(s) || "global".equals(s)) {
            AppCredential credential = new AppCredential();
            credential.setAppId(s);
            credential.setAppSecret("testpassword");
            credential.setGrantTypes("authorization_code,client_credentials,password,refresh_token,mini_app");
            credential.setAccessExpireTime(3600 * 24);
            credential.setRefreshExpireTime(3600 * 24 * 30);
            credential.setRedirectUrl("http://localhost:3006,http://localhost:3006/auth,http://localhost:8000,http://localhost:8000/auth");
            credential.setScopes("all");
            return new AppCredentialDetail(credential);
        }
        return null;
    }
}
  1. 配置JWT Token服务,用于生成token。配置性的内容,生产环境从来没变过,建议直接拷贝
@Configuration
@EnableAuthorizationServer
public class Oauth2AuthorizationConfig extends AuthorizationServerConfigurerAdapter {
    ...
    @Autowired
    TokenServiceFactory tokenServiceFactory;

    @Autowired
    @Qualifier("authenticationManagerBean")
    private AuthenticationManager authenticationManager;

    @Override
    public void configure(final AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints
                .tokenServices(tokenServiceFactory.JwtTokenService())
                .authenticationManager(authenticationManager);
    }
}

本文通过实现TokenServiceFactory实现Token Service,用于生成token,这部分代码比较琐碎,建议直接运用copy&paste技术(因为生产使用一年了,就没动过)。核心注意3点:

  1. 实现TokenEnhancer用于在token中加入一些自定义的信息
  2. 实现TokenConverter用于将哪些字段放入token
  3. 在accessTokenConverter方法中设置token生成的公钥和密钥
@Service
public class TokenServiceFactory {

    private TokenKeyConfig tokenKeyConfig;
    private ClientDetailsService clientDetailsService;

    @Autowired
    public TokenServiceFactory(
            TokenKeyConfig tokenKeyConfig,
            ClientDetailsService clientDetailsService) {
        this.tokenKeyConfig = tokenKeyConfig;
        this.clientDetailsService = clientDetailsService;
    }

    @Bean
    @Primary
    public AuthorizationServerTokenServices JwtTokenService() {
        final TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        // 设置自定义的TokenEnhancer, TokenConverter,用于在token中增加自定义的内容
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer(), accessTokenConverter()));

        return defaultTokenService(tokenEnhancerChain);
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        final JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setAccessTokenConverter(new CustomAccessTokenConverter());
        // 设置token生成的公钥和密钥,密钥放在resource目录下
        final KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(
                new ClassPathResource(tokenKeyConfig.getPath()), tokenKeyConfig.getPassword().toCharArray());
        converter.setKeyPair(keyStoreKeyFactory.getKeyPair(tokenKeyConfig.getAlias()));

        return converter;
    }

    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }

    @Bean
    public org.springframework.security.oauth2.provider.token.TokenEnhancer tokenEnhancer() {
        return new TokenEnhancer();
    }

    private AuthorizationServerTokenServices defaultTokenService(TokenEnhancerChain tokenEnhancerChain) {
        final DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setTokenStore(tokenStore());
        defaultTokenServices.setSupportRefreshToken(true);
        defaultTokenServices.setTokenEnhancer(tokenEnhancerChain);
        defaultTokenServices.setClientDetailsService(clientDetailsService);
        return defaultTokenServices;
    }
}

密钥生成参考:https://www.jianshu.com/p/c9d5a2aa8648
TokenEnhancer用于在token中加入自定义的内容,通常需要自己实现,会经常随着需求变动修改

public class TokenEnhancer implements org.springframework.security.oauth2.provider.token.TokenEnhancer {

    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        final Map additionalInfo = new HashMap<>(2);

        Authentication userAuthentication = authentication.getUserAuthentication();
        // 如果是client认证,通常是服务间调取认证,token中加入admin角色
        if (authentication.isClientOnly()) {
            List authorities = new ArrayList<>(1);
            authorities.add("admin");
            additionalInfo.put("authorities", authorities);

        } else {
           // 如果是用户认证,token中加入user detail,验证用户名和密码时设置的user detail
            additionalInfo.put("userInfo", userAuthentication.getDetails());

            // 将authorities转换为string类型,便于json序列化
            Set rolesInfo = new HashSet<>(userAuthentication.getAuthorities());
            additionalInfo.put("authorities", rolesInfo.stream().map(auth -> auth.getAuthority()).toArray());
        }

        ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
        return accessToken;
    }
}

CustomAccessTokenConverter通常是把所有claims放到token 中

@Component
public class CustomAccessTokenConverter extends DefaultAccessTokenConverter {

    @Override
    public OAuth2Authentication extractAuthentication(Map claims) {
        OAuth2Authentication authentication = super.extractAuthentication(claims);
        authentication.setDetails(claims);
        return authentication;
    }
}

ResourceServerConfig配置

Spring Security除了作为认证服务器,本身还是资源服务器。本项目Spring Security支持自定义的登陆页面还有一些API接口,因此需要配置ResourceServerConfig

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {

        http.requestMatchers().antMatchers("/api/**", "/users/**", "/static/**")
                .and()
                .authorizeRequests()
                .antMatchers("/api/**", "/users/**").authenticated();
    }
}

Application注解

@SpringBootApplication
@EnableResourceServer                //作为resource server
@EnableGlobalMethodSecurity(prePostEnabled = true)    //允许在方法前加注解确定权限@PreAuthorize("hasAnyAuthority('admin',)")
public class AuthApplication {
    public static void main(String[] args) {
        SpringApplication.run(AuthApplication.class, args);
    }
}

效果

输入登录地址,因为涉及到前后端的配合及通过code换取token的转换,本项目实现了一个前端项目直接使用Spring Security及其登录页面,登录成功后跳回到前端首页,参考文末源码admin-ui。

  1. 登录页面输入localhost:8000然后会跳转到auth服务的登录页面如下图,用户名可以输入admin/admin实际上可以随便输入,因为上面认证代码实质上没有检验密码


    登录页面
  1. 登录后成功页面, JWT Token保存在cookie中。


    2.png

如果想直接访问Spring Security中的登录页面,则启动你的Spring Security服务访问下面连接(假设服务端口为5000),也可以下载文末awesome-admin中的admin-service源码启动config、registry、auth服务。登录后可以成功跳转到redirect_uri并且生成了code,用此code可以通过postman获取token。

  1. 使用下面登录url, 输入用户名和密码均为admin后跳转的redirect_uri
    http://localhost:5000/auth/oauth/authorize?response_type=code&client_id=app&redirect_uri=http://localhost:3006/auth

  2. 跳转后可以看到连接中的code


    登录成功后获取code
  3. 以code为参数通过API获取token,注意要使用basic auth认证,用户名和密码就是你之前在 ClientDetailsService中配置的app和appsecret
    http://localhost:5000/auth/oauth/token?grant_type=authorization_code&code=pYIspF&redirect_uri=http://localhost:3006/auth

    Code获取JWT Token API

面向Copy&Paste编程

  1. awesome-admin源码
    https://gitee.com/awesome-engineer/awesome-admin

你可能感兴趣的:(Spring Security OAuth2.0自定义登录页面 + JWT Token配置)