我前面的文章曾自己引入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
这两个概念很重要,待会都是围绕这两个角色进行编写代码。
授权服务器配置类
@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
这里的Username和password对应MyAuthorizationServerConfigurerAdapter里configure配置的
这里的Username和password则对应MyUserDetailsService里的User对象
填写后点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穿
还有什么问题的话,欢迎留言询问,我会在工作不忙的时候尽快回复,觉得有用的话点个赞把~