认证中心整合
# pom
<!--security-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--MyBatis Plus 依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
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
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
# 认证中心
# 1、定义集成认证实体
@Data
public class IntegrationAuthenticationEntity {
/**
* 请求登录认证类型
*/
private String authType;
/**
* 请求登录认证参数集合
*/
private Map<String, String[]> authParameters;
public String getAuthParameter(String paramter) {
String[] values = this.authParameters.get(paramter);
if (values != null && values.length > 0) {
return values[0];
}
return null;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 2、定义集成认证-认证器接口
public interface IntegrationAuthenticator {
/**
* 处理集成认证
* @param entity 集成认证实体
* @return 用户表实体
*/
UserPojo authenticate(IntegrationAuthenticationEntity entity);
/**
* 预处理
* @param entity 集成认证实体
*/
void prepare(IntegrationAuthenticationEntity entity);
/**
* 判断是否支持集成认证类型
* @param entity 集成认证实体
*/
boolean support(IntegrationAuthenticationEntity entity);
/**
* 认证结束后执行
* @param entity 集成认证实体
*/
void complete(IntegrationAuthenticationEntity entity);
}
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
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
# 3、定义集成认证-认证器抽象类
public abstract class AbstractPreparableIntegrationAuthenticator implements IntegrationAuthenticator {
@Override
public void prepare(IntegrationAuthenticationEntity entity) {
}
@Override
public void complete(IntegrationAuthenticationEntity entity) {
}
}
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
# 4、定义集成认证上下文
public class IntegrationAuthenticationContext {
private static ThreadLocal<IntegrationAuthenticationEntity> holder = new ThreadLocal<>();
public static void set(IntegrationAuthenticationEntity entity){
holder.set(entity);
}
public static IntegrationAuthenticationEntity get(){
return holder.get();
}
public static void clear(){
holder.remove();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 5、定义集成认证拦截器
@Component
public class IntegrationAuthenticationFilter extends GenericFilterBean implements ApplicationContextAware {
private static final String AUTH_TYPE_PARM_NAME = "auth_type";//登录类型参数名
private static final String OAUTH_TOKEN_URL = "/oauth/token";//需要拦截的路由
private RequestMatcher requestMatcher;
private ApplicationContext applicationContext;
private Collection<IntegrationAuthenticator> authenticators;
public IntegrationAuthenticationFilter() {
this.requestMatcher = new OrRequestMatcher(
new AntPathRequestMatcher(OAUTH_TOKEN_URL, "GET"),
new AntPathRequestMatcher(OAUTH_TOKEN_URL, "POST")
);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
if (requestMatcher.matches(request)){
RequestParameterWrapper requestParameterWrapper = new RequestParameterWrapper(request);
if (requestParameterWrapper.getParameter("password") == null){
requestParameterWrapper.addParameter("password","");
}
IntegrationAuthenticationEntity entity = new IntegrationAuthenticationEntity();
entity.setAuthType(requestParameterWrapper.getParameter(AUTH_TYPE_PARM_NAME));
entity.setAuthParameters(requestParameterWrapper.getParameterMap());
IntegrationAuthenticationContext.set(entity);
try {
this.prepare(entity);
filterChain.doFilter(requestParameterWrapper,servletResponse);
this.complete(entity);
} finally {
IntegrationAuthenticationContext.clear();
}
}
else {
filterChain.doFilter(servletRequest,servletResponse);
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
/**
* 认证前回调
* @param entity 集成认证实体
*/
private void prepare(IntegrationAuthenticationEntity entity) {
if (entity != null){
synchronized (this){
Map<String, IntegrationAuthenticator> map = applicationContext.getBeansOfType(IntegrationAuthenticator.class);
if (map != null){
this.authenticators = map.values();
}
}
}
if (this.authenticators == null){
this.authenticators = new ArrayList<>();
}
for (IntegrationAuthenticator authenticator : this.authenticators){
if (authenticator.support(entity)){
authenticator.prepare(entity);
}
}
}
/**
* 认证结束后回调
* @param entity 集成认证实体
*/
private void complete(IntegrationAuthenticationEntity entity) {
for (IntegrationAuthenticator authenticator: authenticators) {
if(authenticator.support(entity)){
authenticator.complete(entity);
}
}
}
/**
* 用途:在拦截时给Request添加参数
* Cloud OAuth2 密码模式需要判断Request是否存在password参数,
* 如果不存在会抛异常结束认证
* 所以在调用doFilter方法前添加password参数
*/
class RequestParameterWrapper extends HttpServletRequestWrapper {
private Map<String, String[]> params = new HashMap<String, String[]>();
public RequestParameterWrapper(HttpServletRequest request) {
super(request);
this.params.putAll(request.getParameterMap());
}
public RequestParameterWrapper(HttpServletRequest request, Map<String, Object> extraParams) {
this(request);
addParameters(extraParams);
}
public void addParameters(Map<String, Object> extraParams) {
for (Map.Entry<String, Object> entry : extraParams.entrySet()) {
addParameter(entry.getKey(), entry.getValue());
}
}
@Override
public String getParameter(String name) {
String[]values = params.get(name);
if(values == null || values.length == 0) {
return null;
}
return values[0];
}
@Override
public String[] getParameterValues(String name) {
return params.get(name);
}
@Override
public Map<String, String[]> getParameterMap() {
return params;
}
public void addParameter(String name, Object value) {
if (value != null) {
if (value instanceof String[]) {
params.put(name, (String[]) value);
} else if (value instanceof String) {
params.put(name, new String[]{(String) value});
} else {
params.put(name, new String[]{String.valueOf(value)});
}
}
}
}
}
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
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
# 6、定义用户表实体
@Data
public class UserPojo implements Serializable {
private Integer id;
private String name;
private String mobile;
private String mail;
private String pwd;
public UserPojo() {
}
}
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
# 7、集成认证-用户细节服务
@Service
public class IntegrationUserDetailsService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
private List<IntegrationAuthenticator> authenticators;
@Autowired(required = false)
public void setIntegrationAuthenticators(List<IntegrationAuthenticator> authenticators) {
this.authenticators = authenticators;
}
@Override
public UserDetails loadUserByUsername(String str) throws UsernameNotFoundException {
IntegrationAuthenticationEntity entity = IntegrationAuthenticationContext.get();
if (entity == null){
entity = new IntegrationAuthenticationEntity();
}
UserPojo pojo = this.authenticate(entity);
if (pojo == null){
throw new UsernameNotFoundException("此账号不存在!");
}
User user = new User(pojo.getName(), passwordEncoder.encode(entity.getAuthParameter("password")), AuthorityUtils.commaSeparatedStringToAuthorityList("ROOT_USER"));
return user;
}
private UserPojo authenticate(IntegrationAuthenticationEntity entity) {
if (this.authenticators != null) {
for (IntegrationAuthenticator authenticator : authenticators) {
if (authenticator.support(entity)) {
return authenticator.authenticate(entity);
}
}
}
throw new OAuth2Exception("无效的auth_type参数值!");
}
}
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
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
# 8、启用Security
项目需要用到密码模式所以将AuthenticationManager添加到容器中,不需要用到密码模式,这步骤可以跳过
@EnableWebSecurity
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
# 9、启用授权服务器
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfigurer extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private IntegrationUserDetailsService integrationUserDetailsService;
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager)
.userDetailsService(integrationUserDetailsService);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients()
.tokenKeyAccess("isAuthenticated()")
.checkTokenAccess("permitAll()");
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("client")
.secret(passwordEncoder().encode("server"))
.authorizedGrantTypes("password","refresh_token")
.scopes("all");
}
}
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
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
客户端应该查询MySql,每种客户端的accessToken和RfreshToken应该都不一样
# 10、添加JwtTokenConfig
@Configuration
public class JwtTokenConfig {
@Bean
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
accessTokenConverter.setSigningKey("dev");
return accessTokenConverter;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 11、创建MySql表
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
`name` varchar(100) NOT NULL COMMENT '昵称',
`mobile` varchar(100) NOT NULL COMMENT '手机号',
`mail` varchar(100) NOT NULL COMMENT '邮箱',
`pwd` varchar(100) NOT NULL COMMENT '密码',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=83 DEFAULT CHARSET=utf8 COMMENT='用户表';
INSERT INTO user VALUES(NULL,'root','13555555555','10086@qq.com','$2a$10$hcMi5tIUGGGim/Xe0Z7q4e5Zz3QlK.EAek3an3nZf0B.ZdN0GJgSe')
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
# 12、定义Mapper
@Mapper
public interface UserMapper {
@Select("SELECT * FROM user WHERE name = #{name}")
public UserPojo findByName(String name);
@Select("SELECT * FROM user WHERE mobile = #{mobile}")
public UserPojo findByMobile(String mobile);
@Select("SELECT * FROM user WHERE mail = #{mail}")
public UserPojo findByMail(String mail);
}
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
# 13、定义密码登录认证器
@Component
@Primary
public class UsernamePasswordAuthenticator extends AbstractPreparableIntegrationAuthenticator {
@Autowired
private UserMapper mapper;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserPojo authenticate(IntegrationAuthenticationEntity entity) {
String name = entity.getAuthParameter("name");
String pwd = entity.getAuthParameter("pwd");
if(name == null || pwd == null){
throw new OAuth2Exception("用户名或密码不能为空");
}
UserPojo pojo = mapper.findByName(name);
if(passwordEncoder.matches(pwd,pojo.getPwd())){
return pojo;
}
return null;
}
@Override
public boolean support(IntegrationAuthenticationEntity entity) {
return StringUtils.isEmpty(entity.getAuthType());
}
}
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
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
# postman测试效果
启动服务器,发现自带接口
如果用了JwtToken,这个token就很长了,不过还是可以接受的,毕竟我们现在只是整合了认证中心,资源中心还是需要用jwt来校验这个token。
# 短信登录认证器
@Component
public class SmsAuthenticator extends AbstractPreparableIntegrationAuthenticator {
private final static String AUTH_TYPE = "sms";
@Autowired
private UserMapper mapper;
@Override
public UserPojo authenticate(IntegrationAuthenticationEntity entity) {
String mobile = entity.getAuthParameter("mobile");
if(StringUtils.isEmpty(mobile)){
throw new OAuth2Exception("手机号不能为空");
}
String code = entity.getAuthParameter("code");
//测试项目,所以将验证码顶死为:1234
if(! "1234".equals(code)){
throw new OAuth2Exception("验证码错误或已过期");
}
return mapper.findByMobile(mobile);
}
@Override
public boolean support(IntegrationAuthenticationEntity entity) {
return AUTH_TYPE.equals(entity.getAuthType());
}
}
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
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
# 资源认证
前面我已经整合了认证中心,接下来资源中心也要整合了
# pom
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
1
2
3
4
2
3
4
# ResourceServerConfig
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private TokenStore jwtTokenStore;
@Bean
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
accessTokenConverter.setSigningKey("dev");
accessTokenConverter.setVerifierKey("dev");
return accessTokenConverter;
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.tokenStore(jwtTokenStore);
resources.accessDeniedHandler(restfulAccessDeniedHandler());
resources.authenticationEntryPoint(restAuthenticationEntryPoint());
}
@Override
public void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.csrf()// 由于使用的是JWT,我们这里不需要csrf
.disable()
.sessionManagement()// 基于token,所以不需要session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers(HttpMethod.GET, // 允许对于网站静态资源的无授权访问
"/",
"/*.html",
"/favicon.ico",
"/**/*.html",
"/**/*.css",
"/**/*.js",
"/swagger-resources/**",
"/v2/api-docs/**",
"/**/v2/api-docs/**",
"/v2/api-docs**",
"/**/v2/api-docs**"
)
.permitAll()
.antMatchers("/admin/login", "/admin/register")// 对登录注册要允许匿名访问
.permitAll()
.antMatchers(HttpMethod.OPTIONS)//跨域请求会先进行一次options请求
.permitAll()
// .antMatchers("/**")//测试时全部运行访问
// .permitAll()
.anyRequest()// 除上面外的所有请求全部需要鉴权认证
.authenticated();
// 禁用缓存
httpSecurity.headers().cacheControl();
httpSecurity.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler())
.authenticationEntryPoint(restAuthenticationEntryPoint());
}
@Bean
public RestfulAccessDeniedHandler restfulAccessDeniedHandler() {
return new RestfulAccessDeniedHandler();
}
@Bean
public RestAuthenticationEntryPoint restAuthenticationEntryPoint() {
return new RestAuthenticationEntryPoint();
}
}
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
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
# 自定义异常返回
# 自定义返回结果:没权限时
public class RestfulAccessDeniedHandler implements AccessDeniedHandler{
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException e) throws IOException, ServletException {
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Cache-Control","no-cache");
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.getWriter().println(JSONUtil.parse(CommonResult.forbidden(e.getMessage())));
response.getWriter().flush();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
# 自定义返回结果:未登录或登录过期
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Cache-Control","no-cache");
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.getWriter().println(JSONUtil.parse(CommonResult.unauthorized(authException.getMessage())));
response.getWriter().flush();
}
}
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
这时去资源中心请求资源带上token就可以了,token记得带上前缀
Bearer eJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MjU1NDIzNjIsInVzZXJfbmFtZSI6InJvb3QiLCJhdXRob3JpdGllcyI6WyJST09UX1VTRVIiXSwianRpIjoiZDBmZTAxYTItZjUzZS00ZmY4LThiZTUtMjg1ODkzYmRmNWY2IiwiY2xpZW50X2lkIjoibWluaS1wcm9ncmFtLWFwcCIsInNjb3BlIjpbImFsbCJdfQ.9AFrmCLR-wecS8O0B4e2qiFgNWjRWjtPcYv0wVnw5fc
1
# 总结
1、流程思路:通过拦截器IntegrationAuthenticationFilter拦截所有oauth/token请求,根据类型参数(参数名:auth_type)匹配对应认证器(在所有继承AbstractPreparableIntegrationAuthenticator类中调用support方法筛选),在匹配的成功的认证器调用authenticate方法执行用户认证处理。 2、扩展其他登录方式只要实现自定义的IntegrationAuthenticator就好了。
3、项目源码 https://gitee.com/yugu/cloud-... (opens new window)
# 参考
Spring Cloud OAuth2 优雅集成登录 - SegmentFault 思否 (opens new window)
帮我改善此页面 (opens new window)
上次更新: 2021/07/06, 08:34:17