一 前言
在一些体量较小的的系统,我们一般使用单体系统就可以满足项目的需求。而基于token的认证方式基本成为了现有主要认证方式,本文主要介绍通过jwt来实现springsecurity单体项目的认证方式,希望能对你有一些启发。
二 JDK的keytool生成jks证书
jks是java keystore的简称,是java的数字证书库,查看证书私钥需要密码,避免私钥一名文的形式出现在代码中
2.1 生成证书
在命令行(cmd)中执行命令:keytool -genkeypair -alias mytest -keyalg RSA -keypass mypass -keystore mytest.jks -storepass mypass [文件保存路径(可有可无)],没有指定目录的话,生成的证书在当前命令所在文件夹(生成的mytest.jks证书中包含我们的密钥 :公钥和私钥
根据实际情况生成相应的证书:
命令说明:
-genkey 在用户主目录中创建一个默认文件".keystore",还会产生一个mykey的别名,mykey中包含用户的公钥、私钥和证书
(在没有指定生成位置的情况下,keystore会存在用户系统默认目录,如:对于window xp系统,会生成在系统的C:/Documents and Settings/UserName/文件名为“.keystore”)
-alias 产生别名
-keystore 指定密钥库的名称(产生的各类信息将不在.keystore文件中)
-keyalg 指定密钥的算法 (如 RSA DSA(如果不指定默认采用DSA))
-validity 指定创建的证书有效期多少天
-keysize 指定密钥长度
-storepass 指定密钥库的密码(获取keystore信息所需的密码)
-keypass 指定别名条目的密码(私钥的密码)
-dname 指定证书拥有者信息 例如: "CN=名字与姓氏,OU=组织单位名称,O=组织名称,L=城市或区域名称,ST=州或省份名称,C=单位的两字母国家代码"
-list 显示密钥库中的证书信息 keytool -list -v -keystore 指定keystore -storepass 密码
-v 显示密钥库中的证书详细信息
-export 将别名指定的证书导出到文件 keytool -export -alias 需要导出的别名 -keystore 指定keystore -file 指定导出的证书位置及证书名称 -storepass 密码
-file 参数指定导出到文件的文件名
-delete 删除密钥库中某条目 keytool -delete -alias 指定需删除的别 -keystore 指定keystore -storepass 密码
-printcert 查看导出的证书信息 keytool -printcert -file yushan.crt
-keypasswd 修改密钥库中指定条目口令 keytool -keypasswd -alias 需修改的别名 -keypass 旧密码 -new 新密码 -storepass keystore密码 -keystore sage
-storepasswd 修改keystore口令 keytool -storepasswd -keystore e:/yushan.keystore(需修改口令的keystore) -storepass 123456(原始密码) -new yushan(新密码)
-import 将已签名数字证书导入密钥库 keytool -import -alias 指定导入条目的别名 -keystore 指定keystore -file 需导入的证书
-storetype 生成证书类型(格式:标准pkcs12)
2.2 查看证书
命令行(cmd)中执行命令"keytool -list -v -keystore mytest.jks"命令查看JKS中生成的证书的详细信息
命令行(cmd)中执行命令"keytool -list -rfc -keystore mytest.jks"则可以将证书信息打印到cmd窗口上
2.3 导出证书
如果要导出cer证书。则利用“keytool -alias test -exportcert -keystore mytest.jks -file test.cer”,导出证书,并可以双击打开证书查看证书信息;
openssl 是一个加解密工具包,我们可以使用 openssl 来导出公钥信息。 安装 openssl :http://slproweb.com/products/Win32OpenSSL.html;
cmd 进入你要导出的文件所在目录执行如下命令:
keytool -list -rfc --keystore mytest.jks | openssl x509 -inform pem -pubkey
公钥内容
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsMMTPjqIxcBjdwTphn3B
iwDtPAO6hs+Y7cQfeSNwotUPadQROTVcPqD5SXsbyAKcPQDrgJw8xVFaJqxyXsP5
KX95YYQujjyrfG0e3fojD8s3rV20Y8FKt+P+WNaI7IQPcr6stVOFvr+G0AhhO8Rt
V6F+Y88jhOxo8kvSQfLqWLONEsP+cAaVBiiJ2igFNQ4fP4NBF+uog1P+PVSoddOM
NgEP+aahKXPbVClhCbWK0JfS1+BRIfucgC5jYPykYXeK642Fo4Z4hsr/ySWShBy6
5sXOvn2VQRY4JQ0FPSb6d5lUW5PAsOmpLFxdVOU6zrBB38wV2KJRB5sZOA9cesl+
DQIDAQAB
-----END PUBLIC KEY-----
三 项目整合jwt和springsecurity
将上面生成jks文件导入resource目录里面
定义基础配置
@Data
@ConfigurationProperties(AuthProperties.AUTH_PREFIX)
public class AuthProperties {
public static final String AUTH_PREFIX = "config.auth.jwt";
/**
* token默认过期时间7天
*/
private static final Long DEFAULT_EXPIRE_TIME = 3600000L;
/**
* 忽略校验的path
*/
private Set<String> ignorePaths;
/**
* token过期时间(单位毫秒)
*/
private Long expireInTimeMills = DEFAULT_EXPIRE_TIME;
/**
* rsa加密相关配置
*/
private RsaProperties rsa;
@Data
public static class RsaProperties{
/**
* 加密密码
*/
private String passWord;
/**
* 文件路径
*/
private String keyPairPath;
/**
* 别名
*/
private String alias;
}
}
yml配置基础配置
config:
auth:
jwt:
expire-in-time-mills: 3600000
rsa:
password: mypass
alias: auth
key-pair-path: auth.jks
ignore-paths:
- /auth/authenticate
- /user/testToken
- /upload/**
- /*.html
- /webjars/**
- /swagger-resources/**
- /v3/**
- /v2/**
申明jwt生成和解析的相关接口及其实现
public interface AccessTokenManager {
/**
* 创造token
* @param authentication
* @return
*/
AccessToken createToken(Authentication authentication);
/**
* 校验token是否有效
* @param accessToken
* @return
*/
Boolean verify(String accessToken);
}
@Slf4j
public class JwtAccessTokenManager implements AccessTokenManager {
private final AuthProperties authProperties;
private final JWTSigner jwtSigner;
public JwtAccessTokenManager(AuthProperties authProperties) throws Exception {
this.authProperties = authProperties;
KeyStore keyStore = KeyStore.getInstance("JKS");
AuthProperties.RsaProperties rsaProperties = authProperties.getRsa();
keyStore.load(ResourceUtil.getStream(rsaProperties.getKeyPairPath()),rsaProperties.getPassWord().toCharArray());
PrivateKey privateKey = (PrivateKey) keyStore.getKey(rsaProperties.getAlias(), rsaProperties.getPassWord().toCharArray());
PublicKey publicKey = keyStore.getCertificate(rsaProperties.getAlias()).getPublicKey();
KeyPair keyPair = new KeyPair(publicKey, privateKey);
this.jwtSigner=JWTSignerUtil.createSigner(AlgorithmUtil.getAlgorithm(SignAlgorithm.SHA256withRSA.getValue()),keyPair);
}
@Override
public AccessToken createToken(Authentication authentication) {
AccessToken accessToken = new AccessToken();
accessToken.setTokenType(AccessTokenType.JWT.name());
accessToken.setExpireInTimeMills(authProperties.getExpireInTimeMills());
HashMap<String, Object> payloads = new HashMap<String, Object>();
payloads.put(RegisteredPayload.AUDIENCE, authentication.getName());
payloads.put(RegisteredPayload.JWT_ID, IdUtil.fastUUID());
DateTime expiredAt = DateUtil.offset(new Date(), DateField.MILLISECOND, Convert.toInt(authProperties.getExpireInTimeMills()));
payloads.put(RegisteredPayload.EXPIRES_AT, expiredAt);
String token = JWTUtil.createToken(payloads, this.jwtSigner);
accessToken.setAccessToken(token);
return accessToken;
}
@Override
public Boolean verify(String accessToken) {
JWT jwt = JWT.of(accessToken);
jwt.setSigner(jwtSigner);
if (!jwt.verify()){
return Boolean.FALSE;
}
JWTValidator validator = JWTValidator.of(jwt);
try {
validator.validateAlgorithm();
validator.validateDate();
} catch (Exception e) {
log.error("[jwk校验失败],失败原因是:{}",e.getMessage());
return Boolean.FALSE;
}
return Boolean.TRUE;
}
}
@Data
public class AccessToken {
private String accessToken;
private String tokenType;
private Long expireInTimeMills;
}
public enum AccessTokenType {
JWT;
}
自定义token获取接口
@RestController
@RequestMapping("/auth")
public class AuthenticationEndpoint {
public static final String AUTH_ENDPOINT = "/authenticate";
private final AuthenticationManager authenticationManager;
private final AccessTokenManager accessTokenManager;
public AuthenticationEndpoint(AccessTokenManager accessTokenManager, AuthenticationManager authenticationManager){
this.authenticationManager=authenticationManager;
this.accessTokenManager=accessTokenManager;
}
@PostMapping(AUTH_ENDPOINT)
public R authentication(AuthenticationRequest authenticationRequest){
String userName = authenticationRequest.getUserName();
String password = authenticationRequest.getPassword();
if (!StrUtil.isAllNotBlank(userName,password)){
throw new BadCredentialsException("用户名或密码错误");
}
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userName, password);
Authentication authenticate = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
if (!authenticate.isAuthenticated()){
throw new BadCredentialsException("user " + authenticationRequest.getUserName() + " authenticated failed.");
}
final AccessToken token = accessTokenManager.createToken(authenticate);
return R.ok(token);
}
}
自定义token校验过滤器
@Slf4j
public class AuthenticationFilter extends OncePerRequestFilter {
private static final String BEARER = "bearer";
private final AuthProperties authProperties;
private final AccessTokenManager accessTokenManager;
private final AntPathMatcher antPathMatcher;
public AuthenticationFilter(AuthProperties authProperties, AccessTokenManager accessTokenManager, AntPathMatcher antPathMatcher){
this.authProperties=authProperties;
this.accessTokenManager=accessTokenManager;
this.antPathMatcher=antPathMatcher;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 判断当前请求是否为忽略的路径
Set<String> ignorePaths = authProperties.getIgnorePaths();
if (CollUtil.isNotEmpty(ignorePaths)){
for (String ignorePath : ignorePaths) {
if (antPathMatcher.match(ignorePath,request.getRequestURI())){
filterChain.doFilter(request, response);
return;
}
}
}
// token校验
String bearerToken = request.getHeader(Header.AUTHORIZATION.getValue());
if (StrUtil.isBlank(bearerToken)){
response.setStatus(HttpStatus.UNAUTHORIZED.value());
throw new InsufficientAuthenticationException("unauthorized request.");
}
final String accessToken = bearerToken.trim().substring(BEARER.length()).trim();
boolean valid = false;
try {
valid = accessTokenManager.verify(accessToken);
} catch (Exception e) {
log.warn("verify access token [{}] failed.", accessToken);
throw new InsufficientAuthenticationException("invalid access token + [ " + accessToken + " ].");
}
if (!valid) {
throw new InsufficientAuthenticationException("invalid access token + [ " + accessToken + " ].");
}
final String account = request.getParameter(ACCOUNT);
if (StringUtils.isBlank(account)) {
filterChain.doFilter(request, response);
return;
}
//校验是否本人
final String audience = JWT.of(accessToken).getPayload(RegisteredPayload.AUDIENCE).toString();
if (!account.equalsIgnoreCase(audience)) {
throw new AccessDeniedException("invalid account. parameter [ " + account + " ]. account in token [ " + audience + " ].");
}
filterChain.doFilter(request, response);
}
}
重写userService实现自定义用户查询
public class DBUserService implements UserDetailsService {
private PtUserService ptUserService;
public DBUserService(PtUserService ptUserService) {
this.ptUserService = ptUserService;
}
@Override
public UserDetails loadUserByUsername(String account) throws UsernameNotFoundException {
final PtUser ptUser = ptUserService.findOneByAccount(account);
if (ptUser == null) {
throw new UsernameNotFoundException("account [" + account + " ] not found.");
}
return new User(ptUser.getAccount(), ptUser.getPassword(),true, true, true, true, new ArrayList<>());
}
}
自定义springsecurity配置
@Configuration
@EnableWebSecurity
@EnableConfigurationProperties(AuthProperties.class)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private PtUserService ptUserService;
public WebSecurityConfig(PtUserService ptUserService) {
this.ptUserService = ptUserService;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
@Bean
public UserDetailsService userDetailsService() {
return new DBUserService(ptUserService);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
AuthProperties authProperties() {
return new AuthProperties();
}
@Bean
AccessTokenManager accessTokenManager() throws Exception {
return new JwtAccessTokenManager(authProperties());
}
@Bean
public AuthenticationFilter authenticationFilter(AccessTokenManager accessTokenManager) {
return new AuthenticationFilter(authProperties(),accessTokenManager,new AntPathMatcher());
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.csrf().disable()
.authorizeRequests()
.antMatchers("/**").permitAll()
.anyRequest().authenticated().and()
.exceptionHandling()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
httpSecurity.addFilterBefore(authenticationFilter(accessTokenManager()), UsernamePasswordAuthenticationFilter.class);
}
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass(DispatcherServlet.class)
@Bean
public GlobalExceptionController globalExceptionController(ErrorAttributes errorAttributes, ServerProperties serverProperties) {
return new GlobalExceptionController(errorAttributes, serverProperties.getError());
}
}
定义全局异常拦截,对登录和token校验结果适配项目的响应
这里可以参考笔者的这篇文章自定义springboot组件实现异常处理
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler({BadCredentialsException.class})
public ResponseEntity<R> handleBadCredentialsException(BadCredentialsException e) {
return new ResponseEntity(R.failed("用户名或密码错误"), HttpStatus.BAD_REQUEST);
}
}
@RequestMapping("${server.error.path:${error.path:/error}}")
public class GlobalExceptionController extends BasicErrorController {
private final ErrorAttributes errorAttributes;
public GlobalExceptionController(ErrorAttributes errorAttributes, ErrorProperties errorProperties) {
super(errorAttributes, errorProperties);
this.errorAttributes = errorAttributes;
}
@Override
@RequestMapping
@ResponseBody
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = this.getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity(status);
} else {
ServletWebRequest servletWebRequest = new ServletWebRequest(request);
Throwable error = errorAttributes.getError(servletWebRequest);
if (error!=null){
error = ExceptionUtil.getRootCause(error);
if (error instanceof AuthenticationException){
R result = new R();
result.setMsg("用户凭证已过期");
result.setCode(CommonConstants.FAIL);
Map<String, Object> body = BeanUtil.beanToMap(result);
return new ResponseEntity(body, HttpStatus.UNAUTHORIZED);
}
}
Map<String, Object> body = this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity(body, status);
}
}
}
四 客户端调用
- 生成token
- token使用
五 结语
案例源码
这种适合体量较小的项目,在分布式项目中我们一般整合oauth2来实现token体系的认证,后面笔者会专门写一些oauth2的专题文章,敬请期待