SpringSecurity OAuth2 整合JWT,获取token,解析token(密码模式为例子)

我前面的文章曾自己引入JWT,并且定义规则,并用于权限判断(想了解的可以看看源码),这篇将重点记录下,当我发现SpringSecurity可以使用业界公认的OAuth2规范来自动生成token,并且可以整合JWT,我是如何把它用于项目里的。

源码地址https://github.com/longqiyuye925/blogServer

直接从依赖开始:

这些都是我所用到的相关包,注意下我这篇文章的时间,如果和你当前时间很接近而且你是新建的SpringBoot项目,不要使用最新版本,应该使用2.5.X,否则你会报cloud和SpringBoot版本不匹配的一些错误。

        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.16.0</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.76</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
            <version>2.5.0</version>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.3.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter</artifactId>
            <version>3.0.3</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
            <version>2.2.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
            <version>2.2.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>2020.0.3</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

粗略看一下官网对OAuth2的介绍

https://projects.spring.io/spring-security-oauth/docs/oauth2.html

SpringSecurity OAuth2 整合JWT,获取token,解析token(密码模式为例子)
SpringSecurity OAuth2 整合JWT,获取token,解析token(密码模式为例子)

这两个概念很重要,待会都是围绕这两个角色进行编写代码。

授权服务器配置类

@Configuration
@EnableAuthorizationServer
public class MyAuthorizationServerConfigurerAdapter extends AuthorizationServerConfigurerAdapter {
    /**
     * password密码模式需要使用此认证管理器
     */
    @Autowired
    AuthenticationManager authenticationManager;
    /**
     * token工厂
     */
    @Autowired
    TokenStore jwtTokenStore;
    /**
     * 用于OAuth2生成的token和JWT生成的token进行一个转换
     * @return
     */
    @Autowired
    JwtAccessTokenConverter jwtAccessTokenConverter;
    //用户登录的时候会进行身份验证,并带着权限返回
    @Autowired
    MyUserDetailsService myUserDetailsService;
    /**
     * 想扩展JWT中存储的内容的话,就实现这个token增强器
     */
    @Autowired
    JWTokenEnhancer jwTokenEnhancer;


    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("test1")//客户端ID
                .secret(new MyPasswordEncoder().encode("test1111"))//(受信任的客户端需要)客户端机密,如果有的话。
                .authorizedGrantTypes("password", "refresh_token", "authorization_code", "client_credentials")//授权客户端使用的授权类型。默认值为空。
                .accessTokenValiditySeconds(3600)
                .refreshTokenValiditySeconds(3600)
                .scopes("all")//客户端受限的范围。如果范围未定义或为空(默认),则客户端不受范围限制。
                .autoApprove(true)  // false 跳转到授权页面手动点击授权,true 不用手动授权,直接响应授权码,
                .redirectUris("http://www.baidu.com/");// 客户端回调地址

    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
        List<TokenEnhancer> enhancers = new ArrayList<>();
        enhancers.add(jwTokenEnhancer);
        enhancers.add(jwtAccessTokenConverter);
        enhancerChain.setTokenEnhancers(enhancers);

        endpoints.authenticationManager(authenticationManager)
                .tokenStore(jwtTokenStore)
                .accessTokenConverter(jwtAccessTokenConverter)
                .userDetailsService(myUserDetailsService);
    }
}

这个bean的控制反转代码,放到WebSecurityConfigurerAdapter的子类里就行

/**
 * password密码模式需要使用此认证管理器
 */
@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
}

JWTokenConfig token工厂

@Configuration
public class JWTokenConfig {
    @Autowired
    JwtAccessTokenConverter jwtAccessTokenConverter;

    /**
     * token工厂
     * @return
     */
    @Bean
    public TokenStore jwtTokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter);
    }

}

JWTConfig 用于OAuth2生成的token和JWT生成的token进行一个转换

@Configuration
public class JWTConfig {
    /**
     * 用于OAuth2生成的token和JWT生成的token进行一个转换
     * @return
     */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey("wsc");
        return jwtAccessTokenConverter;
    }
}

MyUserDetailsService 用户登录的时候会进行身份验证,并带着权限返回

/**
 * @Description: 用户登录的时候会进行身份验证,并带着权限返回
 * @author: zf
 * @Param:
 * @Return:
 * @Date: 2021 06 2021/6/17
 */
@Service
public class MyUserDetailsService implements UserDetailsService {
    @Autowired
    private MyPasswordEncoder myPasswordEncoder;
    @Autowired
    ControlAccount controlAccount;
    //账户未被锁定
    Boolean accountNonLocked = true;
    Log log = LogFactory.getLog(MyUserDetailsService.class);

    /**
     * @param username
     * @return
     * @throws UsernameNotFoundException
     * @description 此方法会在用户登录的时候执行
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("MyUserDetailsService:loadUserByUsername come in username:" + username);

        //检查下当前账户是不是被锁定,三次就是锁定
        Integer num = controlAccount.getLockTable().get(username);
        if (null != num && 3 <= num.intValue()) {
            accountNonLocked = false;
        }
        //根据名称查出所拥有的角儿

        //根据角色查出对应的权限

        //把每一个权限构造如SimpleGrantedAuthority
        List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
        if ("account".equals(username)) {
            GrantedAuthority grantedAuthority1 = new SimpleGrantedAuthority("customer");
            GrantedAuthority grantedAuthority2 = new SimpleGrantedAuthority("all");
            grantedAuthorities.add(grantedAuthority1);
            grantedAuthorities.add(grantedAuthority2);
        } else {
            throw new BadCredentialsException("用户不存在");
        }
        //返回security的user对象
        //this(username, password, true, true, true, true, authorities);
        //PasswordEncoder passwordEncoder = new MyPasswordEncoder();
        User user = new User("account", myPasswordEncoder.encode("wushichao")
                , true, true, true, accountNonLocked, grantedAuthorities);

        return user;
    }
}

JWTokenEnhancer 用来扩展JWT中存储的内容

@Component
public class JWTokenEnhancer implements TokenEnhancer {
    /**
     * 想扩展JWT中存储的内容的话,就实现这个token增强器
     * @param oAuth2AccessToken
     * @param oAuth2Authentication
     * @return
     */
    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
        Map<String, Object> info = new HashMap<>();
        info.put("hello", "security!!!");
        ((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(info);
        return oAuth2AccessToken;
    }
}

接下来是资源服务器配置类

MyResourceServerConfigurerAdapter 中重写configure(HttpSecurity http)方法,主要就是控制请求路径的权限访问,相当于把未引入前OAuth2前WebSecurityConfigurerAdapter的子类里的配置挪到了此处。

@Configuration
@EnableResourceServer
public class MyResourceServerConfigurerAdapter extends ResourceServerConfigurerAdapter {
    @Autowired
    private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
    @Autowired
    private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
	//@Autowired
    //private MyAbstractSecurityInterceptor myAbstractSecurityInterceptor;
    //@Autowired
    //MyAccessDeniedHandler myAccessDeniedHandler;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .formLogin() // 表单登录
                .loginProcessingUrl("/login") // 处理表单登录 URL 注意这里即使是使用默认的登录页面也一定要指定,否则资源服务器会找不到路径报404
                .successHandler(myAuthenticationSuccessHandler)//登录成功后处理
                .failureHandler(myAuthenticationFailureHandler)//登录失败后处理
                .and()
                .exceptionHandling()
                //.accessDeniedHandler(myAccessDeniedHandler)//无权限处理
                .and()
                .authorizeRequests()// 授权配置
                .antMatchers("/oauth/**", "/login", "/login/**", "logout/**")
                .permitAll()//都放行
                .anyRequest() // 其它所有请求
                .authenticated() // 都需要认证
        ;
        //http.addFilterBefore(myAbstractSecurityInterceptor, FilterSecurityInterceptor.class);
    }
}

MyAuthenticationSuccessHandler 登录成功后的处理

@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Autowired
    ControlAccount controlAccount;
    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {

        //登录成功后清除掉锁定的次数
        String username = httpServletRequest.getParameter("username");
        controlAccount.getLockTable().remove(username);
        //自己编码的JWTtoken
        String token = JWTUtil.createToken("account");
        Response response = new Response();
        response.setStatus("666");
        response.setMsg("登录成功");
        response.setToken(token);
        httpServletResponse.setContentType("text/json;charset=utf-8");
        httpServletResponse.getWriter().write(JSON.toJSONString(response));
    }
}

MyAuthenticationFailureHandler 登录失败后的处理

@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Autowired
    ControlAccount controlAccount;

    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        Response response = new Response();

        if (e instanceof AccountExpiredException) {
            //账号过期
            response.setStatus("fail001");
            response.setMsg("账号过期");
        } else if (e instanceof BadCredentialsException) {
            String username = httpServletRequest.getParameter("username");
            Integer num = controlAccount.getLockTable().get(username);
            if (null != num) {
                controlAccount.putLockAccount(username, new Integer(num.intValue() + 1));
            } else {
                controlAccount.putLockAccount(username, new Integer(1));
            }
            //密码错误
            response.setStatus("fail002");
            response.setMsg("密码错误");
        } else if (e instanceof CredentialsExpiredException) {
            //密码过期
            response.setStatus("fail003");
            response.setMsg("密码过期");
        } else if (e instanceof DisabledException) {
            //账号不可用
            response.setStatus("fail004");
            response.setMsg("账号不可用");
        } else if (e instanceof LockedException) {
            //账号锁定
            response.setStatus("fail005");
            response.setMsg("账号锁定");
        } else if (e instanceof InternalAuthenticationServiceException) {
            //用户不存在
            response.setStatus("fail006");
            response.setMsg("用户不存在");
        } else {
            //其他错误
            response.setStatus("fail000");
            response.setMsg("其他错误");
        }

        httpServletResponse.setContentType("text/json;charset=utf-8");
        httpServletResponse.getWriter().write(JSON.toJSONString(response));
    }
}

最后放上请求报文:

获取token

http://localhost:8080/oauth/token?grant_type=password&username=account&password=wushichao&scope=all

SpringSecurity OAuth2 整合JWT,获取token,解析token(密码模式为例子)

这里的Username和password对应MyAuthorizationServerConfigurerAdapter里configure配置的

SpringSecurity OAuth2 整合JWT,获取token,解析token(密码模式为例子)

SpringSecurity OAuth2 整合JWT,获取token,解析token(密码模式为例子)

这里的Username和password则对应MyUserDetailsService里的User对象
SpringSecurity OAuth2 整合JWT,获取token,解析token(密码模式为例子)

填写后点Send

解析token

对应的java代码

@RestController
public class UserController {
    @RequestMapping("/index")
    public Object index(@AuthenticationPrincipal AuthenticationPrincipal authenticationPrincipal, HttpServletRequest httpServletRequest) {
        String header = httpServletRequest.getHeader("Authorization");
        String token = StringUtils.substringAfter(header, "bearer ");
        return Jwts.parser().setSigningKey("wsc".getBytes(StandardCharsets.UTF_8)).parseClaimsJws(token).getBody();
    }
}

对应的报文

http://localhost:8080/index

注意请求头,有个key为 Authorization,value为 bearer + 空格 + 前面获取的token穿

SpringSecurity OAuth2 整合JWT,获取token,解析token(密码模式为例子)
还有什么问题的话,欢迎留言询问,我会在工作不忙的时候尽快回复,觉得有用的话点个赞把~

上一篇:Spring Security增加OAuth2协议授权模式


下一篇:oauth2