springsecurity-oauth2之认证服务器的编写(二)

这里忽略springcloud-alibaba的搭建,只讲springsecurity的部分。

把生成的密钥对文件放在resources目录下

spring security Actuator认证配置 spring security 认证服务器_前端

认证服务器的代码结构图

1.导包

引入spring-security和oauth2的必须包

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>
        <!--Oauth2-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <!--Oauth2整合jwt-->
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-jose</artifactId>
        </dependency>

2.配置yml

严格来说不需要特别配置,这里只讲两个:
①表示后发现的bean会覆盖之前相同名称的bean

spring:
   main:
     allow-bean-definition-overriding: true

②配置客户端的clientId、clientSecret(非必须),表示这个客户端的标识,资源服务器会根据这个标识去oauth_client_details找其可访问的资源(即resource_ids字段以及scope字段),实际开发中登录时可由前端(或第三方系统)传来。

auth:
  clientId: c1
  clientSecret: secret1

3.配置类AuthServerConfig

@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
    //对称密钥------改用非对称密钥
    //private static final String SIGNING_KEY = "oauth";
    @Resource
    private AuthenticationManager authenticationManager;
    @Resource
    private PasswordEncoder passwordEncoder;
    @Resource
    private AuthorizationCodeServices authorizationCodeServices;
    @Resource
    private JwtAccessTokenConverter accessTokenConverter;
    @Resource
    private TokenStore tokenStore;
    @Resource
    private ClientDetailsService clientDetailsService;

    /**
     * 将客户端信息存储到数据库  ,参照JdbcClientDetailsService源码里数据库建表
     * oauth_client_details
     * @param dataSource
     * @return
     */
    @Bean
    public ClientDetailsService clientDetailsService(DataSource dataSource) {
        JdbcClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
        ((JdbcClientDetailsService) clientDetailsService).setPasswordEncoder(passwordEncoder);
        return clientDetailsService;
    }

    /**
     * 配置一个客户端
     * 既可以通过授权码获取令牌,也可以通过密码获取令牌(有四种方式)
     *
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(clientDetailsService);
    }

    //配置令牌访问端点
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .authenticationManager(authenticationManager)//认证管理(密码模式需要)
                .authorizationCodeServices(authorizationCodeServices)//授权码服务
                .tokenServices(tokenServices())//令牌管理服务
                .allowedTokenEndpointRequestMethods(HttpMethod.POST);//允许post提交访问令牌
    }

    /**
     * 设置授权码模式的授权码如何存取
     *
     * @param dataSource
     * @return
     */
    @Bean
    public AuthorizationCodeServices authorizationCodeServices(DataSource dataSource) {
        return new JdbcAuthorizationCodeServices(dataSource);
    }

    @Bean
    public AuthorizationServerTokenServices tokenServices() {
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setClientDetailsService(clientDetailsService);//客户端详情
        tokenServices.setSupportRefreshToken(true);//支持刷新令牌
        tokenServices.setTokenStore(tokenStore);//令牌存储策略
        //令牌增强  使用jwt令牌
        //使用jwt令牌来替代默认令牌,这样做的好处是携带默认令牌访问资源,每次都要通过授权服务来认证令牌是否有效,
        // 而jwt则可以做到资源服务中自己解析从而判断令牌的有效性;另外一个优势就是jwt令牌有更高的安全性,
        // 可以使用公钥和私钥进行加密和解密,不容易被破解。
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
        tokenServices.setTokenEnhancer(tokenEnhancerChain);

        tokenServices.setAccessTokenValiditySeconds(7200);//令牌默认有效期2小时
        tokenServices.setRefreshTokenValiditySeconds(9600);//刷新令牌默认有效期
        return tokenServices;
    }
    /**
     * 默认是InMemoryTokenStore
     * 也可以采用JdbcTokenStore,JwtTokenStore。
     *
     * @return
     */
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        //加载密钥
        converter.setKeyPair(keyPair());
        return converter;
    }
    /**
     * 读取本地密钥对
     * @return
     */
    @Bean
    public KeyPair keyPair(){
        KeyStoreKeyFactory factory = new KeyStoreKeyFactory(new ClassPathResource("macrosoft.jks"),"420188".toCharArray());
        KeyPair keyPair = factory.getKeyPair("macrosoft", "420188".toCharArray());
       return keyPair;
    }
    /**
     * 令牌的访问策略
     *
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.tokenKeyAccess("permitAll()")//oauth/token_key是公开
                .checkTokenAccess("permitAll()") //oauth/check_token   已经验证了的客户端才能请求check_token 端点
                .allowFormAuthenticationForClients();//表单认证(申请令牌)
    }
}

4.配置类SecurityConfig

@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()//跨域伪造请求无效
                .authorizeRequests().antMatchers("/**").permitAll()//全部放开
                .anyRequest().authenticated();
        //    登陆设置
        http.formLogin();
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService()).passwordEncoder(new BCryptPasswordEncoder());
    }
    /**
     * 获取新的权限信息
     *
     * @return
     */
    @Bean
    public UserDetailsService userDetailsService() {
        return new UserDetailServiceImpl();
    }
    /**
     * 配置密码的加密规则
     *
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    /**
     * 授权服务需要用到这个bean
     *
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }
    /**
     * 忽略拦截url或静态资源文件夹
     *
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers(HttpMethod.GET,
                "/favicon.ico",
                "/*.html",
                "/**/*.css",
                "/**/*.js");
    }
    /**
     * 注入RestTemplate
     * @return
     */
    @Bean
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}

5.自定义数据库操作UserDetailServiceImpl

public class UserDetailServiceImpl implements UserDetailsService {

    @Resource
    private SysUserMapper sysUserMapper;
    @Resource
    private SysRoleMapper sysRoleMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserDetails userDetails = null;
        try {
        	//根据username查询用户信息
            List<SysUserEntity> sysUserList = sysUserMapper.selectByUserNameSysUsers(username);
            if (CollectionUtils.isEmpty(sysUserList)) {
                throw new UsernameNotFoundException("该用户不存在!");
            }
            SysUserEntity sysUserEntity = sysUserList.get(0);
            //根据userId查询角色权限
            String roles = sysRoleMapper.getRoleNameById(sysUserEntity.getId());
            List<GrantedAuthority> authorities = new ArrayList<>();
            String[] arrstr = roles.split(";");
            if (ObjectUtils.isNotEmpty(arrstr)) {
                for (String role : arrstr) {
                    authorities.add(new SimpleGrantedAuthority(role));
                }
            }
            userDetails =
                    new User(username
                            , sysUserEntity.getPassword()
                            , true
                            , true
                            , true
                            , true
                            , authorities);

        } catch (UsernameNotFoundException e) {
            e.printStackTrace();
        }
        return userDetails;
    }
}

6.到这里认证的配置就配完了,这里在放一段登录的代码(service)

@Override
    public RespResult<LoginResultDTO> login(LoginRequestDTO loginRequestDTO) {

        RespResult<LoginResultDTO> result = new RespResult<>();

        ServiceTemplate.executeApiService(getClass(),
                AuthScenceEnum.AUTH_LOGIN_SCENCE,
                loginRequestDTO,
                result,
                request -> {
                    String username = loginRequestDTO.getUsername();
                    String password = loginRequestDTO.getPassword();
                    //1.获取当前服务路径
                    ServiceInstance serviceInstance = loadBalancerClient.choose("macro-auth");
                    URI uri = serviceInstance.getUri();
                    String url = uri + "/auth/oauth/token";
                    //2.组装请求体参数
                    MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
                    body.add("grant_type", "password");
                    body.add("username", username);
                    body.add("password", password);
                    //3.在请求头组装clientId和clientSecret
                    MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
                    //注意:重点,客户端在登录时可以传入clientId、clientSecret(线下给到客户端),那么这样就可以在oauth_client_details配置该clientId的访问资源服务器了
                    //这里目前写死只有一个clientId、clientSecret(即c1和secret1),在oauth_client_details表中配置了c1对应的resource_ids字段的res1,res2,res3
                    //也就是说资源服务器标识为res1或res2或res3的,clientId为c1的皆可访问
                    headers.add("Authorization", getHttpBasic(clientId, clientSecret));
                    //4.组装最终请求参数
                    HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(body, headers);
                    //5.发起登录请求接口
                    ResponseEntity<Map> responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, Map.class);
                    //6.获取返回数据
                    Map map = responseEntity.getBody();
                    boolean flag = (map == null || map.get("access_token") == null || map.get("refresh_token") == null || map.get("jti") == null);
                    AssertUtil.assertFalse(flag, AuthErrorEnum.GET_ACCESS_TOKEN_FAIL);
                    //7.封装返回数据
                    LoginResultDTO loginResultDTO = new LoginResultDTO();
                    loginResultDTO.setAccessToken((String) map.get("access_token"));
                    loginResultDTO.setRefreshToken((String) map.get("refresh_token"));
                    loginResultDTO.setJti((String) map.get("jti"));
                    //8.数据存入redis
                    redisService.setCacheValueForTimes(RedisKeyTemplate.redisUseUserNameAsKey(username), JSON.toJSONString(loginResultDTO),600, TimeUnit.SECONDS);
                    result.setData(loginResultDTO);
                });
        return result;
    }