0%

这里谈谈自定义认证

不使用自定义认证的 WebSecurityConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final Logger logger = LoggerFactory.getLogger(WebSecurityConfig.class);

@Resource(name = "userDetailsService")
private UserDetailsService userDetailsService;
@Resource(name = "passwordEncoder")
private PasswordEncoder passwordEncoder;
//**********************
// 略
//**********************

/**
* 身份验证
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder)
;

}
}

不使用自定义认证的验证类 AbstractUserDetailsAuthenticationProvider

这里只看 authenticate验证的方法,根据自己的理解,我写上了注释,//// 为重点强调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
// 对 supports 方法的二次校验,为空或不等抛出错误
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));

// Determine username,authentication.getPrincipal()获取的就是UserDetail
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
// 默认情况下从缓存中(UserCache接口实现)取出用户信息
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
// 如果从缓存中取不到用户,则设置cacheWasUsed 为false,供后面使用
cacheWasUsed = false;
try {
// retrieveUser是抽象方法,通过子类来实现获取用户的信息,以UserDetails接口形式返回,默认的子类为 DaoAuthenticationProvider
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
//// 这就是为什么 UsernameNotFoundException 抛出信息却获取不到的原因
catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found");
if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials","Bad credentials"));
}
else {
throw notFound;
}
}
Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
}
try {// 验证帐号是否锁定\是否禁用\帐号是否到期
preAuthenticationChecks.check(user);
// 进一步验证凭证 和 密码
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
if (cacheWasUsed) {// 如果是内存用户,则再次获取并验证
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
else {
throw exception;
}
}
//验证凭证是否已过期
postAuthenticationChecks.check(userDetail);
//如果没有缓存则进行缓存,此处的 userCache是 由 NullUserCache 类实现的,名如其义,该类的 putUserInCache 没做任何事
//也可以使用缓存 比如 EhCacheBasedUserCache 或者 SpringCacheBasedUserCache
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
//以下代码主要是把用户的信息和之前用户提交的认证信息重新组合成一个 authentication 实例返回,返回类是 UsernamePasswordAuthenticationToken 类的实例
Object principalToReturn = user;

if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}

return createSuccessAuthentication(principalToReturn, authentication, user);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@startuml
Title "Authentication类图"
interface Principal
interface Authentication
interface AuthenticationManager
interface AuthenticationProvider
abstract class AbstractUserDetailsAuthenticationProvider
class ProviderManager
class DaoAuthenticationProvider
interface UserDetailsService


Principal <|-- Authentication
Authentication <.. AuthenticationManager
AuthenticationManager <|-- ProviderManager
ProviderManager o--> AuthenticationProvider
AuthenticationProvider <|.. AbstractUserDetailsAuthenticationProvider
AbstractUserDetailsAuthenticationProvider <|-- DaoAuthenticationProvider
UserDetailsService <.. AbstractUserDetailsAuthenticationProvider

interface AuthenticationManager{
# Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
abstract class AbstractUserDetailsAuthenticationProvider{
+ public Authentication authenticate(Authentication authentication)
throws AuthenticationException;
+public boolean supports(Class<?> authentication);
}
@enduml

使用自定义认证的 WebSecurityConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final Logger logger = LoggerFactory.getLogger(WebSecurityConfig.class);

private PasswordEncoder passwordEncoder;
@Resource(name = "authenticationProvider")
private AuthenticationProvider authenticationProvider;
//**********************
// 略
//**********************

/**
* 身份验证
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.authenticationProvider(authenticationProvider)
;

}
}

自定义认证类 AuthenticationProviderImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/**
* AuthenticationProviderImpl
* 自定义认证服务
*
* @author maxzhao
* @date 2019-05-23 15:43
*/
@Service("authenticationProvider")
public class AuthenticationProviderImpl implements AuthenticationProvider {
@Resource(name = "userDetailsService")
private UserDetailsService userDetailsService;

@Resource(name = "passwordEncoder")
private PasswordEncoder passwordEncoder;

/**
*
* @param authenticate
* @return
* @throws AuthenticationException
*/
@Override
public Authentication authenticate(Authentication authenticate) throws AuthenticationException {
UsernamePasswordAuthenticationToken token
= (UsernamePasswordAuthenticationToken) authenticate;
String username = token.getName();
UserDetails userDetails = null;

if (username != null) {
userDetails = userDetailsService.loadUserByUsername(username);
}
if (userDetails == null) {
throw new UsernameNotFoundException("用户名/密码无效");
} else if (!userDetails.isEnabled()) {
System.out.println("jinyong用户已被禁用");
throw new DisabledException("用户已被禁用");
} else if (!userDetails.isAccountNonExpired()) {
System.out.println("guoqi账号已过期");
throw new AccountExpiredException("账号已过期");
} else if (!userDetails.isAccountNonLocked()) {
System.out.println("suoding账号已被锁定");
throw new LockedException("账号已被锁定");
} else if (!userDetails.isCredentialsNonExpired()) {
System.out.println("pingzheng凭证已过期");
throw new CredentialsExpiredException("凭证已过期");
}
String password = userDetails.getPassword();
//与authentication里面的credentials相比较 ; todo 加密 token 中的密码
if (!password.equals(token.getCredentials())) {
throw new BadCredentialsException("Invalid username/password");
}
//授权
return new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities());
}

@Override
public boolean supports(Class<?> authentication) {
//返回true后才会执行上面的authenticate方法,这步能确保authentication能正确转换类型
return UsernamePasswordAuthenticationToken.class.equals(authentication);
}
}

本文地址: SpringBoot+Security+JWT进阶:一、自定义认证

推荐

SpringBoot+Security+JWT基础
SpringBoot+Security+JWT进阶:一、自定义认证
SpringBoot+Security+JWT进阶:二、自定义认证实践

本文地址: https://github.com/maxzhao-it/blog/post/53124/

解决方案

问题:UsernameNotFoundException 不能抛出问题不能获取 问题解决
DaoAuthenticationProvider类的retrieveUser 中会重写输出的异常
在这个方法会捕获 UsernameNotFoundException 异常,会执行到父抽象类 AbstractUserDetailsAuthenticationProvider的authenticate方法

  1. 解决方案一:自定义异常
  2. 解决方案二:设置 AbstractUserDetailsAuthenticationProvider 的 hideUserNotFoundExceptions 属性为 true
  3. 解决方案三:直接抛出 BadCredentialsException (最终返回的错误,一般为 message ,抛出的错误只为开发识别)
  4. 解决方案四:自定义认证,实现 AuthenticationProvider 接口

这里谈谈自定义认证

不使用自定义认证的 WebSecurityConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final Logger logger = LoggerFactory.getLogger(WebSecurityConfig.class);

@Resource(name = "userDetailsService")
private UserDetailsService userDetailsService;
@Resource(name = "passwordEncoder")
private PasswordEncoder passwordEncoder;
//**********************
// 略
//**********************

/**
* 身份验证
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder)
;

}
}

不使用自定义认证的验证类 AbstractUserDetailsAuthenticationProvider

这里只看 authenticate验证的方法,根据自己的理解,我写上了注释,//// 为重点强调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
// 对 supports 方法的二次校验,为空或不等抛出错误
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));

// Determine username,authentication.getPrincipal()获取的就是UserDetail
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
// 默认情况下从缓存中(UserCache接口实现)取出用户信息
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
// 如果从缓存中取不到用户,则设置cacheWasUsed 为false,供后面使用
cacheWasUsed = false;
try {
// retrieveUser是抽象方法,通过子类来实现获取用户的信息,以UserDetails接口形式返回,默认的子类为 DaoAuthenticationProvider
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
//// 这就是为什么 UsernameNotFoundException 抛出信息却获取不到的原因
catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found");
if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials","Bad credentials"));
}
else {
throw notFound;
}
}
Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
}
try {// 验证帐号是否锁定\是否禁用\帐号是否到期
preAuthenticationChecks.check(user);
// 进一步验证凭证 和 密码
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
if (cacheWasUsed) {// 如果是内存用户,则再次获取并验证
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
else {
throw exception;
}
}
//验证凭证是否已过期
postAuth//如果没有缓存则进行缓存,此处的 userCache是 由 NullUserCache 类实现的,名如其义,该类的 putUserInCache 没做任何事
//也可以使用缓存 比如 EhCacheBasedUserCache 或者 SpringCacheBasedUserCache
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
//以下代码主要是把用户的信息和之前用户提交的认证信息重新组合成一个 authentication 实例返回,返回类是 UsernamePasswordAuthenticationToken 类的实例
Object principalToReturn = user;

if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}

return createSuccessAuthentication(principalToReturn, authentication, user);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@startuml
Title "Authentication类图"
interface Principal
interface Authentication
interface AuthenticationManager
interface AuthenticationProvider
abstract class AbstractUserDetailsAuthenticationProvider
class ProviderManager
class DaoAuthenticationProvider
interface UserDetailsService


Principal <|-- Authentication
Authentication <.. AuthenticationManager
AuthenticationManager <|-- ProviderManager
ProviderManager o--> AuthenticationProvider
AuthenticationProvider <|.. AbstractUserDetailsAuthenticationProvider
AbstractUserDetailsAuthenticationProvider <|-- DaoAuthenticationProvider
UserDetailsService <.. AbstractUserDetailsAuthenticationProvider

interface AuthenticationManager{
# Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
abstract class AbstractUserDetailsAuthenticationProvider{
+ public Authentication authenticate(Authentication authentication)
throws AuthenticationException;
+public boolean supports(Class<?> authentication);
}
@enduml

使用自定义认证的 WebSecurityConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final Logger logger = LoggerFactory.getLogger(WebSecurityConfig.class);

private PasswordEncoder passwordEncoder;
@Resource(name = "authenticationProvider")
private AuthenticationProvider authenticationProvider;
//**********************
// 略
//**********************

/**
* 身份验证
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.authenticationProvider(authenticationProvider)
;

}
}

自定义认证类 AuthenticationProviderImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/**
* AuthenticationProviderImpl
* 自定义认证服务
*
* @author maxzhao
* @date 2019-05-23 15:43
*/
@Service("authenticationProvider")
public class AuthenticationProviderImpl implements AuthenticationProvider {
@Resource(name = "userDetailsService")
private UserDetailsService userDetailsService;

@Resource(name = "passwordEncoder")
private PasswordEncoder passwordEncoder;

/**
*
* @param authenticate
* @return
* @throws AuthenticationException
*/
@Override
public Authentication authenticate(Authentication authenticate) throws AuthenticationException {
UsernamePasswordAuthenticationToken token
= (UsernamePasswordAuthenticationToken) authenticate;
String username = token.getName();
UserDetails userDetails = null;

if (username != null) {
userDetails = userDetailsService.loadUserByUsername(username);
}
if (userDetails == null) {
throw new UsernameNotFoundException("用户名/密码无效");
} else if (!userDetails.isEnabled()) {
System.out.println("jinyong用户已被禁用");
throw new DisabledException("用户已被禁用");
} else if (!userDetails.isAccountNonExpired()) {
System.out.println("guoqi账号已过期");
throw new AccountExpiredException("账号已过期");
} else if (!userDetails.isAccountNonLocked()) {
System.out.println("suoding账号已被锁定");
throw new LockedException("账号已被锁定");
} else if (!userDetails.isCredentialsNonExpired()) {
System.out.println("pingzheng凭证已过期");
throw new CredentialsExpiredException("凭证已过期");
}
String password = userDetails.getPassword();
//与authentication里面的credentials相比较 ; todo 加密 token 中的密码
if (!password.equals(token.getCredentials())) {
throw new BadCredentialsException("Invalid username/password");
}
//授权
return new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities());
}

@Override
public boolean supports(Class<?> authentication) {
//返回true后才会执行上面的authenticate方法,这步能确保authentication能正确转换类型
return UsernamePasswordAuthenticationToken.class.equals(authentication);
}
}

本文地址: https://github.com/maxzhao-it/blog/post/64981/

First

  • 我第一次使用,所以代码中的注释和说明比较多

优点

  • 由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。

缺点

Security 基本原理

下面每个类或接口的作用,之后都会有代码.

了解 Token结构

Token是一个很长的字符串,中间用点(.)分隔成三个部分。

JWT 的三个部分依次如下。

  • Header(头部)
  • Payload(负载)
  • Signature(签名)

Header(头部)

1
2
3
4
{
"alg": "HS256",
"typ": "JWT"
}

alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT

Payload(负载)

Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。

  • iss (issuer):签发人
  • exp (expiration time):过期时间
  • sub (subject):主题
  • aud (audience):受众
  • nbf (Not Before):生效时间
  • iat (Issued At):签发时间
  • jti (JWT ID):编号

除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。

1
2
3
4
5
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}

注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。

Signature(签名)

Signature 部分是对前两部分的签名,防止数据篡改。

首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。

1
2
3
4
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)

算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用”点”(.)分隔,就可以返回给用户。

思路

  1. 构建
  2. 导入 security 、 jwt 依赖
  3. 用户的验证(service 、 dao 、model)
  4. 实现UserDetailsServiceUserDetails接口
  5. 可选:实现PasswordEncoder 接口(密码加密)
  6. 验证用户登录信息、用户权限的拦截器
  7. security 配置
  8. 登录认证 API

构建

1. 构建

创建个项目

2.导入 security 、 jwt 依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

<project>
<spring-security-jwt.version>1.0.9.RELEASE</spring-security-jwt.version>
<jjwt.version>0.9.1</jjwt.version>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>${spring-security-jwt.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.16</version>
</dependency>
<!--使用啦Lombok插件,需要自己添加 其它需要自己添加了-->
</project>

3.用户的验证(service 、 dao 、model)

model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 用户表
*
*
* @author maxzhao
* @date 2019-6-6 13:53:17
*/
@Accessors(chain = true)
@Data
@Entity
@Table(name = "app_user", schema = "", catalog = "")
@ApiModel(value = "用户表", description = "用户表")
public class AppUser implements Serializable {
private static final long serialVersionUID = -1L;

@Id
@Column(name = "ID",unique = true)
private Long id;

@Basic
@Column(name = "LIVE_ADDRESS")
private String liveAddress;

@Basic
@Column(name = "LOGIN_NAME")
private String loginName;
@Basic
@Column(name = "PASSWORD")
private String password;
/* 省略 其它*/
}

dao

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 用户表
* Repository
*
* @author maxzhao
* @date 2019-5-21 11:17:39
*/
@Repository(value = "appUserRepository")
public interface AppUserRepository extends JpaRepository<AppUser, Long>, JpaSpecificationExecutor<AppUser> {
/**
* 根据登录名 查询当前用户
*
* @param loginName 登录名
* @return
* @author maxzhao
* @date 2019-05-22
*/
List<AppUser> findByLoginNameEquals(String loginName);

}

service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

/**
* 用户表
* Service
*
* @author maxzhao
* @date 2019-5-21 11:17:39
*/
public interface AppUserService {
/**
* 保存
*
* @param appUser
* @return
* @author maxzhao
* @date 2019-06-19
*/
AppUser saveOne(AppUser appUser);

/**
* 根据登录名查询 当前登录用户
*
* @param loginName
* @return
*/
AppUser findByLoginName(String loginName);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 用户表
* ServiceImpl
*
* @author maxzhao
* @date 2019-5-21 11:17:39
*/
@Service(value = "appUserService")
public class AppUserServiceImpl implements AppUserService {

@Resource(name = "appUserRepository")
private AppUserRepository appUserRepository;

@Override
public AppUser saveOne(AppUser appUser) {
return appUserRepository.save(appUser);
}

@Override
public AppUser findByLoginName(String loginName) {
List<AppUser> appUserList = appUserRepository.findByLoginNameEquals(loginName);
return appUserList.size() > 0 ? appUserList.get(0) : null;
}
}

Jwt 工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
/**
* <p>jjwt封装一下方便调用</p>
* <p>JwtTokenUtil</p>
*
* @author maxzhao
* @date 2019-07-04 13:30
*/
public class JwtTokenUtil {

/**
* 密钥
*/
private static final String SECRET = "jwt_secret_gtboot";
private static final String ISS = "gtboot";

/**
* 过期时间是 1800 秒
*/
private static final long EXPIRATION = 1800L;

public static String createToken(String issuer, String subject, long expiration) {
return createToken(issuer, subject, expiration, null);
}

/**
* 创建 token
*
* @param issuer 签发人
* @param subject 主体,即用户信息的JSON
* @param expiration 有效时间(秒)
* @param claims 自定义参数
* @return
* @description todo https://www.cnblogs.com/wangshouchang/p/9551748.html
*/
public static String createToken(String issuer, String subject, long expiration, Claims claims) {
return Jwts.builder()
// JWT_ID:是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
// .setId(id)
// 签名算法以及密匙
.signWith(SignatureAlgorithm.HS512, SECRET)
// 自定义属性
.setClaims(null)
// 主题:代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,roldid之类的,作为什么用户的唯一标志。
.setSubject(subject)
// 受众
// .setAudience(loginName)
// 签发人
.setIssuer(Optional.ofNullable(issuer).orElse(ISS))
// 签发时间
.setIssuedAt(new Date())
// 过期时间
.setExpiration(new Date(System.currentTimeMillis() + (expiration > 0 ? expiration : EXPIRATION) * 1000))
.compact();
}

/**
* 从 token 中获取主题信息
*
* @param token
* @return
*/
public static String getProperties(String token) {
return getTokenBody(token).getSubject();
}

/**
* 校验是否过期
*
* @param token
* @return
*/
public static boolean isExpiration(String token) {
return getTokenBody(token).getExpiration().before(new Date());
}

/**
* 获得 token 的 body
*
* @param token
* @return
*/
private static Claims getTokenBody(String token) {
return Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody();
}
}

4.实现UserDetailsServiceUserDetails接口

UserDetailsService

可以把角色相关禁用掉,然后修改参数.

我也会把角色相关操作,以及表的 sql 放到最后.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/**
* 加载特定于用户的数据的核心接口。
* 它作为用户DAO在整个框架中使用,是DaoAuthenticationProvider使用的策略。
* 该接口只需要一个只读方法,这简化了对新数据访问策略的支持。
*
* @author maxzhao
*/
@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {
private static final Logger logger = LoggerFactory.getLogger(UserDetailsServiceImpl.class);
/**
* 用户操作服务
*/
@Resource(name = "appUserService")
private AppUserService appUserService;

/**
* 用户角色服务
*/
@Resource(name = "appRoleService")
private AppRoleService appRoleService;

/**
* 根据用户登录名定位用户。
*
* @param loginName
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String loginName) throws UsernameNotFoundException {

UserDetails userDetails = null;
try {
AppUser appUser = appUserService.findByLoginName(loginName);
if (appUser != null) {
// 查询当前用户的权限
List<AppRole> appRoleList = appRoleService.findByUserId(appUser.getId());
Collection<GrantedAuthority> authorities = new ArrayList<>();
for (AppRole appRole : appRoleList) {
SimpleGrantedAuthority grant = new SimpleGrantedAuthority(appRole.getConstName());
authorities.add(grant);
}
//封装自定义UserDetails类
userDetails = new UserDetailsImpl(appUser, authorities);
} else {
throw new UsernameNotFoundException("该用户不存在!");
}
} catch (Exception e) {
logger.error(e.getMessage());
}
return userDetails;
}

}

UserDetails

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
/**
* 自定义用户身份信息
* 提供核心用户信息。
* 出于安全目的,Spring Security不直接使用实现。它们只是存储用户信息,这些信息稍后封装到身份验证对象中。这允许将非安全相关的用户信息(如电子邮件地址、电话号码等)存储在一个方便的位置。
* 具体实现必须特别注意,以确保每个方法的非空契约都得到了执行。有关参考实现(您可能希望在代码中对其进行扩展或使用),请参见User。
*
* @author maxzhao
* @date 2019-05-22
*/
public class UserDetailsImpl implements UserDetails {
private static final long serialVersionUID = 1L;
/**
* 用户信息
*/
private AppUser appUser;
/**
* 用户角色
*/
private Collection<? extends GrantedAuthority> authorities;

public UserDetailsImpl(AppUser appUser, Collection<? extends GrantedAuthority> authorities) {
super();
this.appUser = appUser;
this.authorities = authorities;
}

/**
* 返回用户所有角色的封装,一个Role对应一个GrantedAuthority
*
* @return 返回授予用户的权限。
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
/* Collection<GrantedAuthority> authorities = new ArrayList<>();
String username = this.getUsername();
if (username != null) {
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(username);
authorities.add(authority);
}*/
return authorities;
}
/**
* 返回用于验证用户身份的密码。
*
* @return Returns the password used to authenticate the user.
*/
@Override
public String getPassword() {
return appUser.getPassword();
}
/**
* @return
*/
@Override
public String getUsername() {
return appUser.getLoginName();
}
/**
* 判断账号是否已经过期,默认没有过期
*
* @return true 没有过期
*/
@Override
public boolean isAccountNonExpired() {
return appUser.getExpiration() == null || appUser.getExpiration().before(new Date());
}
/**
* 判断账号是否被锁定,默认没有锁定
*
* @return true 没有锁定 false 锁定
*/
@Override
public boolean isAccountNonLocked() {
return appUser.getLockStatus() == null || appUser.getLockStatus() == 0;
}
/**
* todo 判断信用凭证是否过期,默认没有过期
*
* @return
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 判断账号是否可用,默认可用
*
* @return
*/
@Override
public boolean isEnabled() {
return appUser.getDelStatus() == 0;
}
}

5.可选:实现PasswordEncoder 接口(密码加密)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
/**
* PasswordEncoderImpl
*
* @author maxzhao
* @date 2019-05-23 15:55
*/
@Service("passwordEncoder")
public class PasswordEncoderImpl implements PasswordEncoder {
private final int strength;
private final SecureRandom random;
private Pattern BCRYPT_PATTERN;
private Logger logger;

/**
* 构造函数用于设置不同的加密过程
*/
public PasswordEncoderImpl() {
this(-1);
}

public PasswordEncoderImpl(int strength) {
this(strength, null);
}

public PasswordEncoderImpl(int strength, SecureRandom random) {
this.BCRYPT_PATTERN = Pattern.compile("\\A\\$2a?\\$\\d\\d\\$[./0-9A-Za-z]{53}");
this.logger = LoggerFactory.getLogger(this.getClass());
if (strength == -1 || strength >= 4 && strength <= 31) {
this.strength = strength;
this.random = random;
} else {
throw new IllegalArgumentException("Bad strength");
}
}

/**
* 对原始密码进行编码。通常,一个好的编码算法应用SHA-1或更大的哈希值和一个8字节或更大的随机生成的salt。
* Encode the raw password. Generally, a good encoding algorithm applies a SHA-1 or greater hash combined with an 8-byte or greater randomly generated salt.
*
* @param rawPassword
* @return
*/
@Override
public String encode(CharSequence rawPassword) {
String salt;
if (this.strength > 0) {
if (this.random != null) {
salt = BCrypt.gensalt(this.strength, this.random);
} else {
salt = BCrypt.gensalt(this.strength);
}
} else {
salt = BCrypt.gensalt();
}

return BCrypt.hashpw(rawPassword.toString(), salt);
}

/**
* 验证从存储中获得的已编码密码在经过编码后是否与提交的原始密码匹配。
* 如果密码匹配,返回true;如果密码不匹配,返回false。存储的密码本身永远不会被解码。
*
* @param rawPassword the raw password to encode and match
* @param encodedPassword the encoded password from storage to compare with
* @return
*/
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (encodedPassword != null && encodedPassword.length() != 0) {
if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
this.logger.warn("Encoded password does not look like BCrypt");
return false;
} else {
return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
}
} else {
this.logger.warn("Empty encoded password");
return false;
}
}

/**
* 如果为了更好的安全性,应该再次对已编码的密码进行编码,则返回true,否则为false。
*
* @param encodedPassword the encoded password to check
* @return Returns true if the encoded password should be encoded again for better security, else false. The default implementation always returns false.
*/
@Override
public boolean upgradeEncoding(String encodedPassword) {
return false;
}
}

6.验证用户登录信息、用户权限的拦截器

  • JwtAuthenticationFilter用户账号的验证
  • JwtAuthorizationFilter用户权限的验证

JwtAuthenticationFilter继承于UsernamePasswordAuthenticationFilter
该拦截器用于获取用户登录的信息,只需创建一个 token并调用 authenticationManager.authenticate() spring-security
去进行验证就可以了,不用自己查数据库再对比密码了,这一步交给spring去操作。

这个操作有点像是shirosubject.login(new UsernamePasswordToken()),验证的事情交给框架。

7.security 配置

8.登录认证 API

配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
spring:
datasource:
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/maxzhao_ittest?charset=utf8mb4&useSSL=false
username: maxzhao
password: maxzhao
main:
allow-bean-definition-overriding: true

jpa:
database: MYSQL
database-plinatform: org.hibernate.dialect.MySQL5InnoDBDialect
show-sql: true
generate-ddl: true
open-in-view: false

hibernate:
ddl-auto: update
# naming-strategy: org.hibernate.cfg.ImprovedNamingStrategy
properties:
#不加此配置,获取不到当前currentsession
hibernate:
current_session_context_class: org.springframework.orm.hibernate5.SpringSessionContext
dialect: org.hibernate.dialect.MySQL5Dialect
# 多数据源配置
gt:
maxzhao:
boot:
#主动开启多数据源
multiDatasourceOpen: true
datasource[0]:
dbName: second
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/pos?charset=utf8mb4&useSSL=false
username: maxzhao
password: maxzhao
datasource[1]:
dbName: third
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/biz?charset=utf8mb4&useSSL=false
username: maxzhao
password: maxzhao

本文地址: SpringBoot+Security+JWT基础

gitee

推荐
SpringBoot+Security+JWT基础
SpringBoot+Security+JWT进阶:一、自定义认证
SpringBoot+Security+JWT进阶:二、自定义认证实践

本文地址: https://github.com/maxzhao-it/blog/post/59134/

前言

阅读下文要了解

注释是按照我初学的时候写的,如果有错误或者不清楚的地方,希望大家能给我指出。

思路

  1. 构建
  2. 导入 security 、 jwt 依赖
  3. 用户的验证(service 、 dao 、model)
  4. 实现UserDetailsServiceUserDetails接口
  5. 可选:实现PasswordEncoder 接口(密码加密)
  6. 验证用户登录信息、用户权限的拦截器
  7. security 配置
  8. 登录认证 API

类图(参考)

构建

略…

导入 security 、 jwt 依赖

略….

用户的验证(service 、 dao 、model)

就是查询用户所有库的逻辑代码

略….

实现UserDetailsServiceUserDetails接口

UserDetailsService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/**
* 加载特定于用户的数据的核心接口。
* 它作为用户DAO在整个框架中使用,是DaoAuthenticationProvider使用的策略。
* 该接口只需要一个只读方法,这简化了对新数据访问策略的支持。
*
* @author maxzhao
*/
@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {
private static final Logger logger = LoggerFactory.getLogger(UserDetailsServiceImpl.class);
/**
* 用户操作服务
*/
@Resource(name = "appUserService")
private AppUserService appUserService;

/**
* 用户角色服务
*/
@Resource(name = "appRoleService")
private AppRoleService appRoleService;
//todo https://segmentfault.com/a/1190000013057238#articleHeader7

/**
* 根据用户登录名定位用户。
*
* @param loginName
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String loginName) throws UsernameNotFoundException {

UserDetails userDetails = null;
try {
AppUser appUser = appUserService.findByLoginName(loginName);
if (appUser != null) {
// 查询当前用户的权限
List<AppRole> appRoleList = appRoleService.findByUserId(appUser.getId());
Collection<GrantedAuthority> authorities = new ArrayList<>();
for (AppRole appRole : appRoleList) {
SimpleGrantedAuthority grant = new SimpleGrantedAuthority(appRole.getConstName());
authorities.add(grant);
}
//封装自定义UserDetails类
userDetails = new UserDetailsImpl(appUser, authorities);
} else {
/**
* UsernameNotFoundException 不能抛出问题不能获取 问题解决
* DaoAuthenticationProvider类的retrieveUser 中会重写输出的异常
* 在这个方法会捕获 UsernameNotFoundException 异常,会执行到父抽象类 AbstractUserDetailsAuthenticationProvider的authenticate方法
* 解决方案一:自定义异常
* 解决方案二:设置 AbstractUserDetailsAuthenticationProvider 的 hideUserNotFoundExceptions 属性为 true
* 解决方案三:直接抛出 BadCredentialsException (最终返回的错误,一般为 message ,抛出的错误只为开发识别)
* 解决方案四:自定义认证,实现 AuthenticationProvider 接口
*/
throw new BadCredentialsException("该用户不存在!");
}
} catch (Exception e) {
logger.error(e.getMessage());
}
return userDetails;
}
}

UserDetails

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/**
* 自定义用户身份信息
* 提供核心用户信息。
* 出于安全目的,Spring Security不直接使用实现。它们只是存储用户信息,这些信息稍后封装到身份验证对象中。这允许将非安全相关的用户信息(如电子邮件地址、电话号码等)存储在一个方便的位置。
* 具体实现必须特别注意,以确保每个方法的非空契约都得到了执行。有关参考实现(您可能希望在代码中对其进行扩展或使用),请参见User。
*
* @author maxzhao
* @date 2019-05-22
*/
public class UserDetailsImpl implements UserDetails {
private static final long serialVersionUID = 1L;
/**
* 用户信息
*/
private AppUser appUser;
/**
* 用户角色
*/
private Collection<? extends GrantedAuthority> authorities;

public UserDetailsImpl(AppUser appUser, Collection<? extends GrantedAuthority> authorities) {
super();
this.appUser = appUser;
this.authorities = authorities;
}

/**
* 返回用户所有角色的封装,一个Role对应一个GrantedAuthority
*
* @return 返回授予用户的权限。
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
/* Collection<GrantedAuthority> authorities = new ArrayList<>();
String username = this.getUsername();
if (username != null) {
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(username);
authorities.add(authority);
}*/
return authorities;
}

/**
* 返回用于验证用户身份的密码。
*
* @return Returns the password used to authenticate the user.
*/
@Override
public String getPassword() {
return appUser.getPassword();
}

/**
* @return
*/
@Override
public String getUsername() {
return appUser.getLoginName();
}

/**
* 判断账号是否已经过期,默认没有过期
*
* @return true 没有过期
*/
@Override
public boolean isAccountNonExpired() {
return appUser.getExpiration() == null || appUser.getExpiration().before(new Date());
}

/**
* 判断账号是否被锁定,默认没有锁定
*
* @return true 没有锁定 false 锁定
*/
@Override
public boolean isAccountNonLocked() {
return appUser.getLockStatus() == null || appUser.getLockStatus() == 0;
}

/**
* todo 判断信用凭证是否过期,默认没有过期
*
* @return
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}

/**
* 判断账号是否可用,默认可用
*
* @return
*/
@Override
public boolean isEnabled() {
return appUser.getDelStatus() == 0;
}
}

可选:实现PasswordEncoder 接口(密码加密)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
/**
* PasswordEncoderImpl
*
* @author maxzhao
* @date 2019-05-23 15:55
*/
@Service("passwordEncoder")
public class PasswordEncoderImpl implements PasswordEncoder {
private final int strength;
private final SecureRandom random;
private Pattern BCRYPT_PATTERN;
private Logger logger;

/**
* 构造函数用于设置不同的加密过程
*/
public PasswordEncoderImpl() {
this(-1);
}

public PasswordEncoderImpl(int strength) {
this(strength, null);
}

public PasswordEncoderImpl(int strength, SecureRandom random) {
this.BCRYPT_PATTERN = Pattern.compile("\\A\\$2a?\\$\\d\\d\\$[./0-9A-Za-z]{53}");
this.logger = LoggerFactory.getLogger(this.getClass());
if (strength == -1 || strength >= 4 && strength <= 31) {
this.strength = strength;
this.random = random;
} else {
throw new IllegalArgumentException("Bad strength");
}
}

/**
* 对原始密码进行编码。通常,一个好的编码算法应用SHA-1或更大的哈希值和一个8字节或更大的随机生成的salt。
* Encode the raw password. Generally, a good encoding algorithm applies a SHA-1 or greater hash combined with an 8-byte or greater randomly generated salt.
*
* @param rawPassword
* @return
*/
@Override
public String encode(CharSequence rawPassword) {
String salt;
if (this.strength > 0) {
if (this.random != null) {
salt = BCrypt.gensalt(this.strength, this.random);
} else {
salt = BCrypt.gensalt(this.strength);
}
} else {
salt = BCrypt.gensalt();
}

return BCrypt.hashpw(rawPassword.toString(), salt);
}

/**
* 验证从存储中获得的已编码密码在经过编码后是否与提交的原始密码匹配。
* 如果密码匹配,返回true;如果密码不匹配,返回false。存储的密码本身永远不会被解码。
*
* @param rawPassword the raw password to encode and match
* @param encodedPassword the encoded password from storage to compare with
* @return
*/
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (encodedPassword != null && encodedPassword.length() != 0) {
if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
this.logger.warn("Encoded password does not look like BCrypt");
return false;
} else {
return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
}
} else {
this.logger.warn("Empty encoded password");
return false;
}
}

/**
* 如果为了更好的安全性,应该再次对已编码的密码进行编码,则返回true,否则为false。
*
* @param encodedPassword the encoded password to check
* @return Returns true if the encoded password should be encoded again for better security, else false. The default implementation always returns false.
*/
@Override
public boolean upgradeEncoding(String encodedPassword) {
return false;
}
}

验证用户登录信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
/**
* <p>用户账号的验证</p>
* <p>JwtAuthenticationFilter</p>
*
* @author maxzhao
* @date 2019-07-04 14:38
*/
@Slf4j
@Component
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;

public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
//todo 与 WebSecurityConfig 中的 loginProcessingUrl 优先级 有带判断
super.setFilterProcessesUrl("/auth/login");
}

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
// return super.attemptAuthentication(request, response);
// 从输入流中获取到登录的信息
try {
AppUser appUser = new ObjectMapper().readValue(request.getInputStream(), AppUser.class);
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(appUser.getLoginName(), appUser.getPassword(), new ArrayList<>())
);
} catch (IOException e) {
log.error("获取登录信息失败");
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(null, null, new ArrayList<>())
);
}
}

//
//

/**
* 成功验证后调用的方法.
* 如果验证成功,就生成token并返回
*
* @param request
* @param response
* @param chain
* @param authResult
* @throws IOException
* @throws ServletException
*/
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws IOException, ServletException {

// 查看源代码会发现调用getPrincipal()方法会返回一个实现了`UserDetails`接口的对象
// 所以就是JwtUser啦
UserDetailsImpl userDetails = (UserDetailsImpl) authResult.getPrincipal();
String token = JwtTokenUtil.createToken("gtboot", userDetails.getUsername(), 1800L);
// 返回创建成功的token
// 但是这里创建的token只是单纯的token
// 按照jwt的规定,最后请求的格式应该是 `Bearer token`
response.setHeader("token", JwtTokenUtil.TOKEN_PREFIX + token);
// response.getWriter().write 中文乱码处理
response.setCharacterEncoding("UTF-8");
response.getWriter().write(ResultObj.getDefaultResponse(JwtTokenUtil.TOKEN_PREFIX + token, "登录成功").toJSON());
}


/**
* 这是验证失败时候调用的方法
*
* @param request
* @param response
* @param failed
* @throws IOException
* @throws ServletException
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
// response.getWriter().write 中文乱码处理
response.setCharacterEncoding("UTF-8");
response.getWriter().write(ResultObj.getResponse("登录失败", "authentication failed, reason: " + failed.getMessage(), ResultObj.ResponseStatus.LOGIN_FAIL).toJSON());
log.error(failed.getMessage());
}
}

用户权限的拦截器

登录成功后才会执行此类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* <p>用户权限的验证</p>
* <p>JwtAuthorizationFilter</p>
*
* @author maxzhao
* @date 2019-07-04 14:39
*/
public class JwtAuthorizationFilter extends BasicAuthenticationFilter {

public JwtAuthorizationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {

String tokenHeader = request.getHeader(JwtTokenUtil.TOKEN_HEADER);
// 如果请求头中没有Authorization信息则直接放行了
if (tokenHeader == null || !tokenHeader.startsWith(JwtTokenUtil.TOKEN_PREFIX)) {
chain.doFilter(request, response);
return;
}
// 如果请求头中有token,则进行解析,并且设置认证信息
SecurityContextHolder.getContext().setAuthentication(getAuthentication(tokenHeader));
super.doFilterInternal(request, response, chain);
}

// 这里从token中获取用户信息并新建一个token
private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) {
String token = tokenHeader.replace(JwtTokenUtil.TOKEN_PREFIX, "");
String username = JwtTokenUtil.getProperties(token);
if (username != null) {
return new UsernamePasswordAuthenticationToken(username, null, new ArrayList<>());
}
return null;
}
}

security 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
/**
* web
* EnableGlobalMethodSecurity 启用方法级的权限认证
*
* @author maxzhao
* @PostMapping
* @PreAuthorize("hasRole('ADMIN')") public String new(){
* return "创建";
* }
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final Logger logger = LoggerFactory.getLogger(WebSecurityConfig.class);

@Resource(name = "userDetailsService")
private UserDetailsService userDetailsService;
@Resource(name = "passwordEncoder")
private PasswordEncoder passwordEncoder;
@Resource(name = "authenticationProvider")
private AuthenticationProvider authenticationProvider;
//todo springboot + spring security验证token进行用户认证 https://blog.csdn.net/menglinjie/article/details/84390503

/**
* 自定义用户认证逻辑
* 设定用户访问权限
* 用户身份可以访问
* 定义需要拦截的URL
* 登录后续操作
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
this.logger.debug("Using start.config.WebSecurityConfig configure(HttpSecurity). ");
String[] permitAllMatchers = new String[]{"/", "/home", "/js/vue.js", "/auth/register"};
http
// 定义哪些URL需要被保护、哪些不需要被保护
.authorizeRequests()
// 设置所有人都可以访问home页面
.antMatchers(permitAllMatchers)
.permitAll()
// 任何请求,登录后可以访问
.anyRequest()
// 验证后可以访问
.authenticated()
.and()
// 用户账号的验证
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
// 用户权限的验证
.addFilter(new JwtAuthorizationFilter(authenticationManager()))
// 不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// 统一异常处理
.exceptionHandling()
// 403 异常
.authenticationEntryPoint(new JwtAuthenticationEntryPoint())
.and()
// 定义当需要用户登录时候,转到的登录页面。
.formLogin()
// 默认/login 在抽象类AbstractAuthenticationFilterConfigurer
// 用户没有登录时,跳转到登录界面,下面的用户未登录时,访问的地址
// 登录失败也跳转到这里
.loginPage("/auth/login/fail")
// 自定义的登录接口,默认为 '/login' this.loginPage , 在抽象类AbstractAuthenticationFilterConfigurer
// 还是走的 security 的接口
// .loginProcessingUrl("/appLogin/login")
.loginProcessingUrl("/auth/login")
// 自定义登录成功后的页面
// .defaultSuccessUrl("/success")
.defaultSuccessUrl("/auth/login/success")
// .failureForwardUrl("/auth/login/fail")
.permitAll()
// 默认username 在类 UsernamePasswordAuthenticationFilter,FormLoginConfigurer初始化方法也设置了默认值
.usernameParameter(UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_USERNAME_KEY)
// 默认password 在类 UsernamePasswordAuthenticationFilter,FormLoginConfigurer初始化方法也设置了默认值
.passwordParameter(UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_PASSWORD_KEY)

/* 在抽象类AbstractAuthenticationFilterConfigurer
.successForwardUrl("")
在抽象类AbstractAuthenticationFilterConfigurer
.failureForwardUrl("")
failureForwardUrl没有设置时,this.failureUrl(this.loginPage + "?error"); , 在抽象类AbstractAuthenticationFilterConfigurer
.failureUrl("")*/
.permitAll()
.and()
.httpBasic();
//暂时禁用CSRF,否则无法提交表单 todo https://www.cnblogs.com/xifengxiaoma/p/10020960.html
http.csrf().disable();
http.logout()
;

}

/**
* 身份验证
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.authenticationProvider(authenticationProvider)
;

}

/**
* 配置拦截资源
*
* @param web
*/
@Override
public void configure(WebSecurity web) {
//解决静态资源被拦截的问题
web.ignoring()
.antMatchers("/js/**", "/css/**", "/img/**");
}

@Bean
CorsConfigurationSource corsConfigurationSource() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
return source;
}
}

登录认证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
/**
* AuthenticationProviderImpl
* 自定义认证服务
*
* @author maxzhao
* @date 2019-05-23 15:43
*/
@Slf4j
@Service("authenticationProvider")
public class AuthenticationProviderImpl implements AuthenticationProvider {
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();

/**
* 验证帐号是否锁定\是否禁用\帐号是否到期
*/
private UserDetailsChecker preAuthenticationChecks = new DefaultPreAuthenticationChecks();
/**
* 验证凭证\密码是否已过期
*/
private UserDetailsChecker postAuthenticationChecks = new DefaultPostAuthenticationChecks();
/**
* 用户缓存,默认没有缓存
* 此处不做缓存
*/
private UserCache userCache = new NullUserCache();
/**
* principal 通常是用户名 或者 UseDetails
* 这里设置控制,默认为 UseDetails
*/
private boolean forcePrincipalAsString = false;
@Resource(name = "userDetailsService")
private UserDetailsService userDetailsService;

@Resource(name = "passwordEncoder")
private PasswordEncoder passwordEncoder;
/**
* @param authentication
* @return
* @throws AuthenticationException
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 对 supports 方法的二次校验,为空或不等抛出错误
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
// 自定义缓存策略
// this.userCache = new GTBootUserCache();

// Determine username,authentication.getPrincipal()获取的就是UserDetail
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
// 默认情况下从缓存中(UserCache接口实现)取出用户信息
boolean cacheWasUsed = true;

UserDetails userDetail = this.userCache.getUserFromCache(username);
if (userDetail == null) {
// 如果从缓存中取不到用户,则设置cacheWasUsed 为false,供后面使用
cacheWasUsed = false;
// retrieveUser是抽象方法,通过子类来实现获取用户的信息,以UserDetails接口形式返回,默认的子类为 DaoAuthenticationProvider
userDetail = userDetailsService.loadUserByUsername(username);
if (userDetail == null) {
log.debug("User '" + username + "' not found");
throw new UsernameNotFoundException("用户不存在");
}
}
try {// 验证帐号是否锁定\是否禁用\帐号是否到期
preAuthenticationChecks.check(userDetail);
// 进一步验证凭证 和 密码
additionalAuthenticationChecks(userDetail,
(UsernamePasswordAuthenticationToken) authentication);
} catch (AuthenticationException exception) {
if (cacheWasUsed) {// 如果是内存用户,则再次获取并验证
cacheWasUsed = false;
userDetail = userDetailsService.loadUserByUsername(username);
preAuthenticationChecks.check(userDetail);
additionalAuthenticationChecks(userDetail, (UsernamePasswordAuthenticationToken) authentication);
} else {
throw exception;
}
}
//验证凭证是否已过期
postAuthenticationChecks.check(userDetail);
//如果没有缓存则进行缓存,此处的 userCache是 由 NullUserCache 类实现的,名如其义,该类的 putUserInCache 没做任何事
//也可以使用缓存 比如 EhCacheBasedUserCache 或者 SpringCacheBasedUserCache
if (!cacheWasUsed) {
this.userCache.putUserInCache(userDetail);
}
//以下代码主要是把用户的信息和之前用户提交的认证信息重新组合成一个 authentication 实例返回,返回类是 UsernamePasswordAuthenticationToken 类的实例
Object principalToReturn = userDetail;

if (forcePrincipalAsString) {
principalToReturn = userDetail.getUsername();
}

return createSuccessAuthentication(principalToReturn, authentication, userDetail);
/*

UsernamePasswordAuthenticationToken token
= (UsernamePasswordAuthenticationToken) authenticate;
String username = token.getName();
UserDetails userDetails = null;

if (username != null) {
userDetails = userDetailsService.loadUserByUsername(username);
}

String password = userDetails.getPassword();
//与authentication里面的credentials相比较 todo 加密 token 的密码
if (!password.equals(token.getCredentials())) {
throw new UsernameNotFoundException("Invalid username/password,密码错误");
}
//TODO 实现 User 缓存
//授权
return new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities());*/
}

@Override
public boolean supports(Class<?> authentication) {
//返回true后才会执行上面的authenticate方法,这步能确保authentication能正确转换类型
return UsernamePasswordAuthenticationToken.class.equals(authentication);
}

public UserCache getUserCache() {
return userCache;
}

/**
* 设置使用的缓存
* @param userCache
*/
public void setUserCache(UserCache userCache) {
this.userCache = userCache;
}

/**
* 验证帐号是否锁定\是否禁用\帐号是否到期
*/
private class DefaultPreAuthenticationChecks implements UserDetailsChecker {
public void check(UserDetails user) {
if (!user.isAccountNonLocked()) {
log.debug("User account is locked");

throw new LockedException(messages.getMessage(
"AuthenticationProviderImpl.locked",
"账号已被锁定"));
}
if (!user.isEnabled()) {
log.debug("User account is disabled");

throw new DisabledException(messages.getMessage(
"AuthenticationProviderImpl.disabled",
"用户已被禁用"));
}
if (!user.isAccountNonExpired()) {
log.debug("User account is expired");
throw new AccountExpiredException(messages.getMessage(
"AuthenticationProviderImpl.expired",
"账号已过期"));
}
}
}

/**
* 验证凭证是否已过期
*/
private class DefaultPostAuthenticationChecks implements UserDetailsChecker {
public void check(UserDetails user) {
if (!user.isCredentialsNonExpired()) {
log.debug("User account credentials have expired");

throw new CredentialsExpiredException(messages.getMessage(
"AuthenticationProviderImpl.credentialsExpired",
"凭证已过期"));
}
}
}

/**
* 验证密码
*
* @param userDetails
* @param authentication
* @throws AuthenticationException
*/
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
if (authentication.getCredentials() == null) {
log.debug("Authentication failed: no credentials provided");

throw new BadCredentialsException(messages.getMessage(
"AuthenticationProviderImpl.badCredentials",
"无效凭证(无效密码)"));
}

String presentedPassword = authentication.getCredentials().toString();

if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
log.debug("Authentication failed: password does not match stored value");

throw new BadCredentialsException(messages.getMessage(
"AuthenticationProviderImpl.badCredentials",
"密码错误"));
}
}

protected Authentication createSuccessAuthentication(Object principal,
Authentication authentication, UserDetails user) {
boolean upgradeEncoding = this.userDetailsService != null
&& this.passwordEncoder.upgradeEncoding(user.getPassword());
String presentedPassword = authentication.getCredentials().toString();
String newPassword = upgradeEncoding ? this.passwordEncoder.encode(presentedPassword) : presentedPassword;
return new UsernamePasswordAuthenticationToken(principal, newPassword, user.getAuthorities());
}
}

我自己小白,看类中写的注释,就懂了。

附录:403 错误返回拦截

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* <p>403响应</p>
* JwtAuthenticationEntryPoint
*
* @author maxzhao
* @date 2019-07-04 18:24
*/
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
/**
* Commences an authentication scheme.
* 启动身份验证方案。.
* <p>填充 populate
* <code>ExceptionTranslationFilter</code> will populate the <code>HttpSession</code>
* attribute named
* <code>AbstractAuthenticationProcessingFilter.SPRING_SECURITY_SAVED_REQUEST_KEY</code>
* with the requested target URL before calling this method.
* <p>
* Implementations should modify the headers on the <code>ServletResponse</code> as
* necessary to commence the authentication process.
*
* @param request that resulted in an <code>AuthenticationException</code>
* @param response so that the user agent can begin authentication
* @param authException that caused the invocation
*/
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
// 么有权限
// Full authentication is required to access this resource
//
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
// String reason = "统一处理,原因:" + authException.getMessage();
response.getWriter().write(ResultObj.getErrorResponse("", "统一处理,原因:" + authException.getMessage()).toJSON());
// response.getWriter().write(new ObjectMapper().writeValueAsString(reason));
}
}

附录:token工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
/**
* <p>jjwt封装一下方便调用</p>
* <p>JwtTokenUtil</p>
*
* @author maxzhao
* @date 2019-07-04 13:30
*/
public class JwtTokenUtil {
public static final String TOKEN_HEADER = "gtboot";
public static final String TOKEN_PREFIX = "gtboot ";

/**
* 密钥
*/
private static final String SECRET = "jwt_secret_gtboot";
private static final String ISS = "gtboot";

/**
* 过期时间是 1800 秒
*/
private static final long EXPIRATION = 1800L;

public static String createToken(String issuer, String subject, long expiration) {
return createToken(issuer, subject, expiration, null);
}

/**
* 创建 token
*
* @param issuer 签发人
* @param subject 主体,即用户信息的JSON
* @param expiration 有效时间(秒)
* @param claims 自定义参数
* @return
* @description todo https://www.cnblogs.com/wangshouchang/p/9551748.html
*/
public static String createToken(String issuer, String subject, long expiration, Claims claims) {
return Jwts.builder()
// JWT_ID:是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
// .setId(id)
// 签名算法以及密匙
.signWith(SignatureAlgorithm.HS512, SECRET)
// 自定义属性
.setClaims(null)
// 主题:代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,roldid之类的,作为什么用户的唯一标志。
.setSubject(subject)
// 受众
// .setAudience(loginName)
// 签发人
.setIssuer(Optional.ofNullable(issuer).orElse(ISS))
// 签发时间
.setIssuedAt(new Date())
// 过期时间
.setExpiration(new Date(System.currentTimeMillis() + (expiration > 0 ? expiration : EXPIRATION) * 1000))
.compact();
}

/**
* 从 token 中获取主题信息
*
* @param token
* @return
*/
public static String getProperties(String token) {
return getTokenBody(token).getSubject();
}


/**
* 校验是否过期
*
* @param token
* @return
*/
public static boolean isExpiration(String token) {
return getTokenBody(token).getExpiration().before(new Date());
}

/**
* 获得 token 的 body
*
* @param token
* @return
*/
private static Claims getTokenBody(String token) {
return Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody();
}
}

本文地址: SpringBoot+Security+JWT进阶:二、自定义认证实践

推荐
SpringBoot+Security+JWT基础
SpringBoot+Security+JWT进阶:一、自定义认证
SpringBoot+Security+JWT进阶:二、自定义认证实践
gitee多数据源
IDEA好用的插件

本文地址: https://github.com/maxzhao-it/blog/post/62112/

前言

Manjaro 安装MySQL8很简单粗暴,为什么还要装 Docker 呢?

滚动更新惹得祸!!!

当前状态 MySQL8.0.16 更新之后需要 GLIBGXX_3.4.26 ,我这里的 GCC 8.3.0-1版本正好没有 GLIBGXX_3.4.26 ,但是还是最新的,搞了一次社区 GCC 9.1.0
版本,操作系统都起不来了。

最后选择Docker,确实简单粗暴!

安装

ArchLinux 安装

1
2
3
sudo pacman -S docker
sudo systemctl enable docker
sudo systemctl start docker

其它安装

Ubuntu Docker 安装
CentOS Docker 安装
Windows Docker 安装
MacOS Docker 安装

安装MySQL8

1
2
3
4
5
6
7
sudo  docker pull mysql
# 启动
sudo docker run --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=maxzhao -d mysql
# 进入容器
sudo docker exec -it mysql bash
#登录mysql 密码 maxzhao
mysql -u root -p

创建用户

1
2
3
4
5
CREATE USER 'maxzhao'@'localhost' IDENTIFIED BY 'maxzhao';
grant all privileges on *.* TO 'maxzhao'@'localhost' WITH GRANT OPTION;

CREATE USER 'maxzhao'@'%' IDENTIFIED BY 'maxzhao';
GRANT ALL PRIVILEGES ON *.* TO 'maxzhao'@'%' WITH GRANT OPTION;

使用

容器外登录MySQL

1
mysql -h127.0.0.1 -uroot -p

查看镜像

1
2
sudo docker images
sudo docker ps

启动镜像

1
sudo docker start mysql

RUN | START

docker run 只在第一次运行时使用,将镜像放到容器中,以后再次启动这个容器时,只需要使用命令docker start 即可。

推荐:

MySQL8.0创建用户及其配置

本文地址: https://github.com/maxzhao-it/blog/post/43589/

前言

Manjaro 安装MySQL8很简单粗暴,为什么还要装 Docker 呢?

滚动更新惹得祸!!!

当前状态 MySQL8.0.16 更新之后需要 GLIBGXX_3.4.26 ,我这里的 GCC 8.3.0-1版本正好没有 GLIBGXX_3.4.26 ,但是还是最新的,搞了一次社区 GCC 9.1.0 版本,操作系统都起不来了。

最后选择Docker,确实简单粗暴!

此处MySql是一个示例,其它安装操作一样的。

安装

ArchLinux 安装

1
2
3
sudo pacman -S docker
sudo systemctl enable docker
sudo systemctl start docker

其它安装

Ubuntu Docker 安装
CentOS Docker 安装
Windows Docker 安装
MacOS Docker 安装

使用其它国内源(可跳过)

直接使用

1
docker pull registry.docker-cn.com/library/mysql

配置源

使用 –registry-mirror 配置 Docker 守护进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
vim /etc/docker/daemon.json
# 写入
{
"registry-mirrors": [
"https://kfwkfulq.mirror.aliyuncs.com",
"https://2lqq34jg.mirror.aliyuncs.com",
"https://pee6w651.mirror.aliyuncs.com",
"https://registry.docker-cn.com",
"http://hub-mirror.c.163.com"
],
"dns": ["8.8.8.8","8.8.4.4"]
}

sudo systemctl restart docker

使用清华大学 yum 源

1
sed  's!//download\.docker\.com!//mirrors.tuna.tsinghua.edu.cn/docker-ce!g' -i /etc/yum.repos.d/docker-ce.repo

安装MySQL8

一、镜像安装

查找 MySql 镜像

1
sudo docker search mysql

获取 MySql 镜像

1
sudo  docker pull mysql

这里下载好的镜像为最新版的 MySql

Downloaded newer image for mysql:latest

启动镜像

1
2
3
4
5
6
7
8
9
# 创建容器
sudo docker run --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=maxzhao -d mysql
# 启动
sudo docker start mysql
# 进入容器
sudo docker exec -it mysql bash
# 登录mysql 密码 maxzhao
# 注意:这是容器内登录
mysql -u root -p

实例

1
2
3
4
5
6
7
8
mkdir -p /opt/mysql8/data /opt/mysql8/logs /opt/mysql8/conf
cd /opt/mysql8
sudo docker run -p 3306:3306 --name mysql \
-v /opt/mysql8/conf:/etc/mysql/conf.d \
-v /opt/mysql8/logs:/logs \
-v /opt/mysql8/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=xxx1 \
-d mysql

命令说明

  • -p 3306:3306:将容器的 3306 端口映射到主机的 3306 端口。
  • -v /opt/mysql8/conf:/etc/mysql/conf.d:将主机当前目录下的 conf/my.cnf 挂载到容器的 /etc/mysql/my.cnf。
  • -v /opt/mysql8/logs:/logs:将主机当前目录下的 logs 目录挂载到容器的 /logs。
  • -v /opt/mysql8/data:/var/lib/mysql :将主机当前目录下的data目录挂载到容器的 /var/lib/mysql 。
  • -e MYSQL_ROOT_PASSWORD=maxzhao:初始化 root 用户的密码。
  • -d mysql:IMAGE 的名称

二、Dockerfile 构建

仔细思考了一下,还是把这章去掉了,毕竟,这篇文章讲的是新手操作。

我们的需求是用MySql而不是操作Docker

其它操作

解决需要添加sudo的问题

1
2
3
4
# sudo gpasswd -a 用户名 用户组
sudo gpasswd -a maxzhao docker
# groups 用户名 查看所在组
groups maxzhao

查看容器运行情况

1
docker ps 

创建用户

1
2
3
4
5
CREATE USER 'maxzhao'@'localhost' IDENTIFIED BY 'maxzhao';
grant all privileges on *.* TO 'maxzhao'@'localhost' WITH GRANT OPTION;

CREATE USER 'maxzhao'@'%' IDENTIFIED BY 'xxx';
GRANT ALL PRIVILEGES ON *.* TO 'maxzhao'@'%' WITH GRANT OPTION;

容器外登录MySQL

1
2
3
# mysql 登录工具
sudo pacman -S mysql-clients
mysql -h127.0.0.1 -uroot -p

查看镜像

1
2
sudo docker images
sudo docker ps

启动镜像

1
sudo docker start mysql

RUN | START

docker run 只在第一次运行时使用,将镜像放到容器中,以后再次启动这个容器时,只需要使用命令docker start 即可。

推荐:

MySQL8.0创建用户及其配置

本文地址: https://github.com/maxzhao-it/blog/post/43590/

前言

什么是Oauth2?

OAuth(Open Authorization,开放授权)是为用户资源的授权定义了一个安全、开放及简单的标准,第三方无需知道用户的账号及密码,就可获取到用户的授权信息。
OAuth2.0是OAuth协议的延续版本,但不向后兼容OAuth 1.0即完全废止了OAuth1.0

注意:对于OAuth2的使用场景,官方文档都提到针对第三方应用,需要强调的是,这个第三方应用,不指的是除了服务本身外的应用,都是第三方应用,即使是自己的应用,也是第三方应用。

了解OAuth2与JWT

  • JWT:JSON Web Token // 是一种具体的Token实现框架;

  • OAuth2:Open Authorization // 是一种授权协议,是规范,不是实现;

  • Spring Security OAuth2:Spring 对 OAuth2 的开源实现,优点是能与Spring Cloud技术栈无缝集成;

  • Spring Security:前身是 Acegi Security ,能够为 Spring企业应用系统提供声明式的安全访问控制;

这里需要理解两个概念:

  • Authentication:用户认证,一般是通过用户名密码来确认用户是否为系统中合法主体;
  • Authorization:用户授权,一般是给系统中合法主体授予相关资源访问权限;

jwt是基于token的认证协议的实现,OAuth2是一种授权框架,提供了一套详细的授权协议标准(指导)。

jwt的基本思路就是用户提供用户名和密码给认证服务器,服务器验证用户提交信息信息的合法性;如果验证成功,会产生并返回一个Token(令牌),用户可以使用这个token访问服务器上受保护的资源。

OAuth

Roles角色

在OAuth2中主要有四个角色,主要如下:

  1. Resource Owner(资源拥有者):用户,即资源的拥有人,想要分享某些资 源给第三方应用
  2. Resource Server(资源服务器):放受保护资源,要访问这些资源,需要获得访问令牌
  3. Authorization server(授权(认证)服务器):授权服务器用于发放访问令牌给客户端
  4. Client(客户端应用(第三方应用)):客户端代表请求资源服务器资源的第三方程序

OAuth2中的其他术语:

  • Client Credentials(客户凭证):客户的clientId 和密码用于认证客户
  • Access Token(访问令牌):授权服务器在接收到客户请求后,颁发的访问令牌
  • Scopes(作用域):客户请求访问令牌时,由资源拥有者额外指定的细分权限(permission)
  • User Agent(用户代理):用户代理。一般就是指浏览器

OAuth2中的令牌类型:

  • Authorization Code Token(授权码):仅用于授权码授权类型,用于交换获取访问令牌和刷新令牌
  • Refresh Token(刷新令牌):用于去授权服务器获取一个新的访问令牌
  • Access Token(访问令牌):用于代表一个用户或服务直接去访问受保护的资源
  • Proof of Possession(PoP) Token(持有证明(pop)令牌):可以校验client是否对Token 有明确的拥有权

授权模式:

  1. 授权码模式(Authorization Code)(正统方式)(支持refresh token)
  2. 授权码简化模式(Implicit)(为web浏览器设计)(不支持refresh token)
  3. Pwd模式(Resource Owner Password Credentials) (基本不用)(支持refresh token)
  4. Client模式(Client Credentials) (为后台api调用设计)(不支持refresh token)
  5. 扩展模式(Extension)(自定义模式)

最常用的grant_type组合有: authorization_code,refresh_token(针对通过浏览器访问的客户端); password,refresh_token(针对移动设备的客户端). implicitclient_credentials在实际中很少使用.

Authorization Grants 认证授权

客户端类型

OAuth 2.0客户端角色被细分为一系列类型和配置,本节将阐述这些类型和配置。

OAuth 2.0规范定义了两种客户端类型:

  • 保密的
  • 公有的

保密的客户端能够对外部保持客户端密码保密。该客户端密码是由授权服务器分配给客户端应用的。为了避免欺骗,该密码是授权服务器用来识别客户端的。例如一个保密的客户端可以是web应用,除了管理员,没有任何人能够访问服务器和看到该密码。

公有的客户端不能使客户端密码保密。比如移动手机应用或桌面应用会将密码嵌入在内部。这样的应用可能被破解,并且泄漏密码。这同于在用户的浏览器上运行的JavaScript应用。用户可以使用一个JavaScript调试器来寻找到应用程序,并查看客户端密码。

客户端配置

OAuth 2.0规范也提到了一系列客户端配置文件。这些配置文件是具体类型的应用程序,这可以是保密或公开的。这些配置文件有:

  • web应用
  • 用户代理
  • 原生

推荐

w3cschool

SpringBoot+Security+JWT基础
SpringBoot+Security+JWT进阶:一、自定义认证
SpringBoot+Security+JWT进阶:二、自定义认证实践

本文地址: https://github.com/maxzhao-it/blog/post/44183/

  • 使用注解@Configuration配置拦截器
  • 继承WebMvcConfigurerAdapterSpringBoot2.0以后可以直接实现WebMvcConfigurer接口,这涉及到 JDK8Interface 新特性
  • 重写addInterceptors添加需要的拦截器地址
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    registry.addInterceptor(new OneInterceptor())
    .addPathPatterns("/testController/**");
    // 拦截多个接口
    // registry.addInterceptor(new OneInterceptor())
    .addPathPatterns("/testController/**")
    .addPathPatterns("/test2Controller/**");
    // 拦截所有接口
    // registry.addInterceptor(new OneInterceptor())
    .addPathPatterns("/*/**");
    super.addInterceptors(registry);
  • 添加拦截器类实现HandlerInterceptor,比如OneInterceptor

本文地址:SpringBoot 拦截器的使用

本文地址: https://github.com/maxzhao-it/blog/post/35633/

Spring Boot是一个命令行工具,用于使用Spring进行快速原型搭建。它允许你运行Groovy脚本,这意味着你可以使用类Java的语法,并且没有那么多的模板代码。

所有版本下载地址
这里下载的版本spring-boot-cli-2.2.0.BUILD-20190222.193142-143-bin.tar.gz

下载完成后解压

1
2
3
4
tar   spring-boot-cli-2.2.0.BUILD-20190222.193142-143-bin.tar.gz  -C  /home/maxzhao/
cd /home/maxzhao
mv spring-2.2.0.BUILD-SNAPSHOT spring-2.2.0-cli
cd spring-2.2.0-cli

当前目录下有INSTALL.txt文件,这里面就是具体的安装方法。
INSTALL.txt文件中讲的很清楚了,spring-boot-cli-2.2.0 需要JDK1.8+

环境变量配置:

1
2
3
4
5
6
7
8
9
10
# 管理员下
# set springboot-cli
vim /etc/profile
#最后输入
export SPRING_HOME="/home/maxzhao/soft/spring-2.2.0-cli"
export PATH="$SPRING_HOME/bin:$PATH"
#保存
source /etc/profile
spring --version
# Spring CLI v2.2.0.BUILD-SNAPSHOT

简单的springboot-cli例子

本文地址 SpringBoot 安装CLI

本文地址: https://github.com/maxzhao-it/blog/post/30842/

前言

Spring Boot好学吗?

当然了,欢迎入坑

学会使用比较容易,想要懂得怎么用的原理就很难了。

起跳准备

推荐几个常用网址:

也有几个常用的插件版本说明:

  • Jetty9.4+

  • Tomcat8.5+

  • Hibernate5.2+

  • Gradle3.4+

还有几个参考

Spring Boot 基本内容简介

组件自动装配

  • 规约大于配置,专注核心业务

  • 这里需要深入了解的是源自spring framework中的 模式注解、@Enable模块、条件装配、加载机制,Spring Boot对它们进行了修改。

外部化配置

  • 一次构建、按需调配、到处运行

  • 这里需要了解的是外部化配置与Environment抽象的关系、生命周期,
    Spring Boot2.0 用的新的API,与1.x版本部分不兼容。

嵌入式容器

  • 内置容器(传统的Servlet容器、Reactive Web)、无需部署、独立运行

  • 为了支持reactive使用场景,内嵌的容器包结构被重构了的幅度有点大。EmbeddedServletContainer被重命名为WebServer,并且org.springframework.boot.context.embedded 包被重定向到了org.springframework.boot.web.embedded包下。举个例子,如果你要使用TomcatEmbeddedServletContainerFactory回调接口来自定义内嵌Tomcat容器,你现在应该使用TomcatServletWebServerFactory

Spring Boot Starter

  • 简化依赖、按需装配、自我包含

  • 依赖管理、条件装配、装配顺序(重要,可以参考autoconfigure包下的spring.factories文件)
    不能再自动配置HazelcastInstance进行缓存。 因此,spring.cache.hazelcast.config属性不再可用。

Production-Ready

Spring Boot 与Java EE 规范

这里可以参考一下这里也可以参考一下

  • Web:servlet
  • Sql:JDBC
  • 数据校验:Bean Validation
  • 缓存:Java Caching API
  • WebSockets:Java API for WebSocket
  • Web Services:有点久远
  • Java管理:JMX
  • 消息:JMS(Apache ActiveMQ

Spring Boot 核心特性

  • 组件自动装配:Web MVC、Web Flux、JDBC……
  • 嵌入式Web容器:Tomcat、Jetty、Undertow以及Web Flux
  • 生产特性:一些非功能特性(指标、健康检查、外部化配置等)

组件自动装配

  • 激活自动装配@EnableAutoConfiguration@SpringBootApplication注解已经默认激活)

  • 配置/METE-INF/spring.factories(前面说到过)
    有了这些Spring Boot的默认配置,我们只有引入相应的依赖,就会被自动装配。

  • 实现:xxxAutoConfiguration

嵌入式Web容器

  • Web Servlet
  • Web Reactive
    两者都是兼容的,也可以相互转换。
    这里我也了解了一下(没有亲自测试),相比于Servlet容器,在获取数据和吞吐量上来说,Web Flux 要比Servlet快很

生产特性

  • 指标/actuator/metrics
  • 健康检查/actuator/health
  • 外部化配置/actuator/configprops
    spring boot 2.0之后,需要有/actuator前缀,也可以自己配置。
    1
    2
    3
    #比如我们想把/actuator/health修改为/healthcheck。
    management.endpoints.web.base-path=/
    management.endpoints.web.path-mapping.health=healthcheck

详情请查看 [SpringBoot2.X性能监控Actuator]

Web 应用

传统Servlet应用

  • Servlet组件:Servlet、Filter、Listener

  • Servlet注册

    • Servlet注解
      • @ServletCo/mponentScan+@WebServlet、@WebFilter、@WebListener
    • Spring Bean
      • @Bean+Servlet、Filter、Listener
    • RegistrationBean(Spring Boot新API)
      • ServletRegistrationBean
      • FilterRegistrationBean
      • ListenerRegistrationBean
  • 异步非阻塞异步Servlet、非阻塞Servlet

数据操作(JDBC、JPA)

功能扩展

组件自动装配

外部化配置

https://blog.csdn.net/javastudyr/article/details/73865362

嵌入式容器

运维管理

其它介绍

自动装配

1
2
3
4
5
6
7
8
9
10
11
@ConditionalOnClass:当类路径下有指定的类的条件下。
@ConditionalOnExpression:基于SpEL 表达式作为判断条件。
@ConditionalOnJava:基于JVM 版本作为判断条件。
@ConditionalOnJndi:在JNDI 存在的条件下查找指定的位置。
@ConditionalOnMissingBean:当容器里没有指定Bean 的情况下。
@ConditionalOnMissingClass:当类路径下没有指定的类的条件下。
@ConditionalOnNotWebApplication:当前项目不是Web 项目的条件下。
@ConditionalOnProperty:指定的属性是否有指定的值。
@ConditionalOnResource:类路径是否有指定的值。
@ConditionalOnSingleCandidate:当指定Bean 在容器中只有一个,或者虽然有多个但是指定首选的Bean。
@ConditionalOnWebApplication:当前项目是Web 项目的条件下。

Spring Boot 2.x 简介


本文地址:https://www.jianshu.com/p/284b4b359dcc

本文地址: https://github.com/maxzhao-it/blog/post/50708/