千寻

道路很长, 开始了就别停下!

0%

Springcloud微服务——基于security和jwt实现认证及鉴权服务

一、需求

  • 1、RESTfull风格的鉴权服务(路线相同的情况下根据请求方式鉴别访问权限)
  • 2、包含用户、角色、权限
  • 3、使用JWT最为token认证方式

二、知识点讲解

2.1 方案

传统的单体应用体系下,应用是一个整体,一般针对所有的请求都会进行权限校验。请求一般会通过一个权限的拦截器进行权限的校验,在登录时将用户信息缓存到 session 中,后续访问则从缓存中获取用户信息

但在微服务架构下,一个应用会被拆分成若干个微应用,每个微应用都需要对访问进行鉴权,每个微应用都需要明确当前访问用户以及其权限。尤其当访问来源不只是浏览器,还包括其他服务的调用时,单体应用架构下的鉴权方式就不是特别合适了。因此在设计架构中,要考虑外部应用接入的场景、用户与服务的鉴权、服务与服务的鉴权等多种鉴权场景。

目前主流的方案由四种

2.1.1 单点登录(SSO)

一次登入,多地使用。这种方案意味着每个面向用户的服务都必须与认证服务交互,进而产生大量琐碎的网络流量和重复的工作,当动辄数十个微应用时,这种方案的弊端会更加明显。

2.1.2 分布式 Session 方案

借助reids或其他共享存储中,将用户认证的信息存储在其中,通常使用用户会话作为 key 来实现的简单分布式哈希映射。当用户访问微服务时,用户数据可以从共享存储中获取。在某些场景下,这种方案很不错,用户登录状态是不透明的。同时也是一个高可用且可扩展的解决方案。这种方案的缺点在于共享存储需要一定保护机制,因此需要通过安全链接来访问,这时解决方案的实现就通常具有相当高的复杂性了。

2.1.3 客户端 Token 方案

令牌在客户端生成,由身份验证服务进行签名,并且必须包含足够的信息,以便可以在所有微服务中建立用户身份。令牌会附加到每个请求上,为微服务提供用户身份验证,这种解决方案的安全性相对较好,但身份验证注销是一个大问题,缓解这种情况的方法可以使用短期令牌和频繁检查认证服务等。对于客户端令牌的编码方案,Borsos 更喜欢使用 JSON Web Tokens(JWT),它足够简单且库支持程度也比较好。

2.1.4 客户端 Token 与 API 网关结合

这个方案意味着所有请求都通过网关,从而有效地隐藏了微服务。 在请求时,网关将原始用户令牌转换为内部会话 ID 令牌。在这种情况下,注销就不是问题,因为网关可以在注销时撤销用户的令牌。

本文就采用方案4,实现微服务体系中用户鉴权及认证服务。

Token的实现方案业界有多套成熟的方案,这其中最主流的是JWT 和 Oauth2.0 两种方式。
下面就基于JWT的方式具体实现。

SpringSecurity

  • AuthenticationManager, 用户认证的管理类,所有的认证请求(比如login)都会通过提交一个token给AuthenticationManager的authenticate()方法来实现。当然事情肯定不是它来做,具体校验动作会由AuthenticationManager将请求转发给具体的实现类来做。根据实现反馈的结果再调用具体的Handler来给用户以反馈。

  • AuthenticationProvider, 认证的具体实现类,一个provider是一种认证方式的实现,比如提交的用户名密码我是通过和DB中查出的user记录做比对实现的,那就有一个DaoProvider;如果我是通过CAS请求单点登录系统实现,那就有一个CASProvider。按照Spring一贯的作风,主流的认证方式它都已经提供了默认实现,比如DAO、LDAP、CAS、OAuth2等。
    前面讲了AuthenticationManager只是一个代理接口,真正的认证就是由AuthenticationProvider来做的。一个AuthenticationManager可以包含多个Provider,每个provider通过实现一个support方法来表示自己支持那种Token的认证。AuthenticationManager默认的实现类是ProviderManager。

  • UserDetailService, 用户认证通过Provider来做,所以Provider需要拿到系统已经保存的认证信息,获取用户信息的接口spring-security抽象成UserDetailService。虽然叫Service,但是我更愿意把它认为是我们系统里经常有的UserDao。

  • AuthenticationToken, 所有提交给AuthenticationManager的认证请求都会被封装成一个Token的实现,比如最容易理解的UsernamePasswordAuthenticationToken。

  • SecurityContext,当用户通过认证之后,就会为这个用户生成一个唯一的SecurityContext,里面包含用户的认证信息Authentication。通过SecurityContext我们可以获取到用户的标识Principle和授权信息GrantedAuthrity。在系统的任何地方只要通过SecurityHolder.getSecruityContext()就可以获取到SecurityContext。在Shiro中通过SecurityUtils.getSubject()到达同样的目的。

三、具体实现

3.1 业务流程

  • 客户端调用登录接口,传入用户名密码。
  • 服务端请求身份认证中心,确认用户名密码正确。
  • 服务端创建JWT,返回给客户端。
  • 客户端拿到 JWT,进行存储(可以存储在缓存中,也可以存储在数据库中,如果是浏览器,可以存储在 Cookie中)在后续请求中,在 HTTP 请求头中加上 JWT。
  • 服务端校验 JWT,校验通过后,返回相关资源和数据。

    3.2 代码

    完整pom文件(项目结构为多模块)
    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
    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
    <artifactId>springcloud</artifactId>
    <groupId>com.lhm</groupId>
    <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>security</artifactId>

    <dependencies>
    <!--web 服务-->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--security-->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    <dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
    </dependency>

    <!--mybatis-plus-->
    <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.1.0</version>
    </dependency>

    <!--mybatis-plus日志-->
    <dependency>
    <groupId>p6spy</groupId>
    <artifactId>p6spy</artifactId>
    <version>3.8.1</version>
    </dependency>

    <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
    </dependency>

    <!-- druid的starter -->
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.9</version>
    </dependency>
    <!-- redis -->
    <dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <!--JSON-->
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.38</version>
    </dependency>
    <!-- StringUtils相关工具类jar包 -->
    <dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.4</version>
    </dependency>

    <!-- lombok -->
    <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <scope>provided</scope>
    </dependency>
    </dependencies>
    </project>

3.3 认证服务


在登入方面,本次使用了security默认提供的表单登陆方式,因此直接从实现
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
package com.lhm.springcloud.security.service.impl;

import com.lhm.springcloud.security.constant.ResultCode;
import com.lhm.springcloud.security.exception.CommonException;
import com.lhm.springcloud.security.pojo.AuthUserDetails;
import com.lhm.springcloud.security.pojo.AuthUserPoJo;
import com.lhm.springcloud.security.service.IUsersService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

/**
* @ClassName UserDetailsServiceImpl
* @Description 实现security提供的 用户信息获取接口 并按照业务增加redis 登陆限制
* @Author Alan
* @Date 2018/5/6 10:26
* @Version 1.0
**/
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
//登入重试时间
@Value("${security.loginAfterTime}")
private Integer loginAfterTime;
@Autowired
private StringRedisTemplate redisTemplate;

@Autowired
private IUsersService iUsersService;

/**
* @Author Alan
* @Description 实现用户信息查询方法 让DaoAuthenticationProvider 获取到数据库获中用户数据
* @Date 11:21 2019/5/6
* @Param [username]
* @return org.springframework.security.core.userdetails.UserDetails
**/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String flagKey = "loginFailFlag:"+username;
String value = redisTemplate.opsForValue().get(flagKey);
if(StringUtils.isNotBlank(value)){
//超过限制次数
throw new UsernameNotFoundException("登录错误次数超过限制,请"+loginAfterTime+"分钟后再试");
}

//查询用户信息
AuthUserPoJo authUserPoJo=iUsersService.findAuthUserByUsername(username);
if(null==authUserPoJo){
throw new UsernameNotFoundException("当前用户不存在");
}
if(authUserPoJo.getRoleInfos()==null || authUserPoJo.getRoleInfos().isEmpty()){
throw new UsernameNotFoundException("当前用户无角色");
}
return new AuthUserDetails(authUserPoJo);
}
}

UserDetailsServiceImpl 最后返回一个拼装好的security用户对象,但为了实现自定义角色与权限管理需要对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
package com.lhm.springcloud.security.pojo;

import com.lhm.springcloud.security.constant.UserConstant;
import com.lhm.springcloud.security.entity.PermissionInfo;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
* @author Exrickx
*/

public class AuthUserDetails extends AuthUserPoJo implements UserDetails {

private static final long serialVersionUID = 1L;

public AuthUserDetails(AuthUserPoJo user) {
if (user != null) {
this.setUserName(user.getUserName());
this.setPassWord(user.getPassWord());
this.setStatus(user.getStatus());
this.setRoleInfos(user.getRoleInfos());
this.setPermissionInfos(user.getPermissionInfos());
}
}

//将角色权限 放入GrantedAuthorit的自定义实现类MyGrantedAuthority中 为权限判定提供数据
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {

List<GrantedAuthority> authorityList = new ArrayList<GrantedAuthority>();
List<PermissionInfo> permissions = this.getPermissionInfos();
if (permissions != null) {
for (PermissionInfo permission : permissions) {
GrantedAuthority grantedAuthority = new MyGrantedAuthority(permission.getPath(), permission.getMethod());
authorityList.add(grantedAuthority);
}
}
return authorityList;
}

@Override
public String getPassword() {
return super.getPassWord();
}

@Override
public String getUsername() {
return super.getUserName();
}


/**
* 账户是否过期
*
* @return
*/
@Override
public boolean isAccountNonExpired() {

return true;
}

/**
* 是否禁用
*
* @return
*/
@Override
public boolean isAccountNonLocked() {

return true;
}

/**
* 密码是否过期
*
* @return
*/
@Override
public boolean isCredentialsNonExpired() {

return true;
}

/**
* 是否启用
*
* @return
*/
@Override
public boolean isEnabled() {
return UserConstant.USER_STATUS_NORMAL.equals(this.getStatus()) ? true : false;
}
}

然后DaoProvider会对比校验并执行相应的结果处理器

登入成功处理器

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
package com.lhm.springcloud.security.handler;

import com.lhm.springcloud.security.constant.ResultCode;
import com.lhm.springcloud.security.pojo.AuthUserDetails;
import com.lhm.springcloud.security.utils.ResUtil;
import com.lhm.springcloud.security.utils.ResponseUtil;
import com.lhm.springcloud.security.utils.TokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;

/**
* @ClassName LoginSuccessHandlerFilter
* @Description 登陆认证成功处理过滤器
* @Author Alan
* @Date 2019/5/6 16:27
* @Version 1.0
**/
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private TokenUtil tokenUtil;

/**
* @Author Alan
* @Description 用户认证成功后 生成token并返回
* @Date 8:50 2019/5/7
* @Param [request, response, authentication]
* @return void
**/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

AuthUserDetails authUserDetails=(AuthUserDetails)authentication.getPrincipal();//从内存中获取当前认证用户信息

//创建token
String accessToken = tokenUtil.createAccessJwtToken(authUserDetails);
String refreshToken = tokenUtil.createRefreshToken(authUserDetails);

HashMap<String,String> map=new HashMap<>();
map.put("accessToken",accessToken);
map.put("refreshToken",refreshToken);

ResponseUtil.out(response, ResUtil.getJsonStr(ResultCode.OK,"登录成功",map));
}
}

登入失败处理器

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
package com.lhm.springcloud.security.handler;

import com.lhm.springcloud.security.constant.ResultCode;
import com.lhm.springcloud.security.utils.ResUtil;
import com.lhm.springcloud.security.utils.ResponseUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

/**
* @ClassName LoginFailureHandler
* @Description 登陆失败处理过滤器
* @Author Alan
* @Date 2019/5/7 9:05
* @Version 1.0
**/
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
//#限制用户登陆错误次数(次)
@Value("${security.loginTimeLimit}")
private Integer loginTimeLimit;
//#错误超过次数后多少分钟后才能继续登录(分钟)
@Value("${security.loginAfterTime}")
private Integer loginAfterTime;

@Autowired
private StringRedisTemplate redisTemplate;

/**
* @Author Alan
* @Description 用户登陆失败处理类 记录用户登陆错误次数
* @Date 9:12 2019/5/7
* @Param [request, response, e]
* @return void
**/
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
if (e instanceof UsernameNotFoundException || e instanceof BadCredentialsException) {
String username = request.getParameter("username");
recordLoginTime(username);
String key = "loginTimeLimit:" + username;
String value = redisTemplate.opsForValue().get(key);
if (StringUtils.isBlank(value)) {
value = "0";
}
//获取已登录错误次数
int loginFailTime = Integer.parseInt(value);
int restLoginTime = loginTimeLimit - loginFailTime;
ResponseUtil.out(response, ResUtil.getJsonStr(ResultCode.BAD_REQUEST, "用户名或密码错误"));
} else if (e instanceof DisabledException) {
ResponseUtil.out(response, ResUtil.getJsonStr(ResultCode.BAD_REQUEST, "账户被禁用,请联系管理员"));
} else {
ResponseUtil.out(response, ResUtil.getJsonStr(ResultCode.BAD_REQUEST, "登录失败"));
}
}
/**
* 判断用户登陆错误次数
*/
public boolean recordLoginTime(String username) {

String key = "loginTimeLimit:" + username;
String flagKey = "loginFailFlag:" + username;
String value = redisTemplate.opsForValue().get(key);
if (StringUtils.isBlank(value)) {
value = "0";
}
//获取已登录错误次数
int loginFailTime = Integer.parseInt(value) + 1;
redisTemplate.opsForValue().set(key, String.valueOf(loginFailTime), loginAfterTime, TimeUnit.MINUTES);
if (loginFailTime >= loginTimeLimit) {

redisTemplate.opsForValue().set(flagKey, "fail", loginAfterTime, TimeUnit.MINUTES);
return false;
}
return true;
}
}

在登入的过程中会对用户的请求间隔时间及失败次数做记录。

3.4 鉴权服务

鉴权的过程分成了两个大的步骤

  • 第一对请求的路径、方法、头部信息进行判断,确认该请求是否需要鉴权
    JWTAuthenticationFilter
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
package com.lhm.springcloud.security.filter;


import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.lhm.springcloud.security.constant.IgnoredUrlsProperties;
import com.lhm.springcloud.security.constant.ResultCode;
import com.lhm.springcloud.security.constant.SecurityConstant;
import com.lhm.springcloud.security.exception.CommonException;
import com.lhm.springcloud.security.pojo.MyGrantedAuthority;
import com.lhm.springcloud.security.utils.ResUtil;
import com.lhm.springcloud.security.utils.ResponseUtil;
import com.lhm.springcloud.security.utils.SpringUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
* JWT过滤器1
*/

public class JWTAuthenticationFilter extends BasicAuthenticationFilter {

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

public JWTAuthenticationFilter(AuthenticationManager authenticationManager, AuthenticationEntryPoint authenticationEntryPoint) {
super(authenticationManager, authenticationEntryPoint);
}

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

IgnoredUrlsProperties ignoredUrlsProperties= SpringUtil.getBean("ignoredUrlsProperties", IgnoredUrlsProperties.class);
String Requesturl=request.getRequestURI();
PathMatcher pathMatcher = new AntPathMatcher();
if(null != ignoredUrlsProperties){
for(String url:ignoredUrlsProperties.getUrls()){
if(pathMatcher.match(url,Requesturl)){
chain.doFilter(request, response);
return;
}
}
}

//获取请求头
String header = request.getHeader(SecurityConstant.HEADER);
//如果请求头中不存在 或 格式不对 则进入下个过滤器
if (StringUtils.isBlank(header) || !header.startsWith(SecurityConstant.TOKEN_SPLIT)) {
chain.doFilter(request, response);
return;
}
try {
UsernamePasswordAuthenticationToken authentication = getAuthentication(request, response);
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (Exception e) {
ResponseUtil.out(response, ResUtil.getJsonStr(ResultCode.BAD_REQUEST, e.getMessage()));
return;
}

chain.doFilter(request, response);
}

/**
* @Author Alan
* @Description 对token进行解析认证
* @Date 11:11 2019/5/7
* @Param [request, response]
* @return org.springframework.security.authentication.UsernamePasswordAuthenticationToken
**/
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request, HttpServletResponse response) throws CommonException {

String token = request.getHeader(SecurityConstant.HEADER);
if (StringUtils.isNotBlank(token)) {
// 解析token
Claims claims = null;
try {
claims = Jwts.parser()
.setSigningKey(SecurityConstant.tokenSigningKey)
.parseClaimsJws(token.replace(SecurityConstant.TOKEN_SPLIT, ""))
.getBody();

//获取用户名
String username = claims.getSubject();

//获取权限
List<MyGrantedAuthority> authorities = new ArrayList<MyGrantedAuthority>();
String authority = claims.get(SecurityConstant.AUTHORITIES).toString();

if (StringUtils.isNotBlank(authority)) {
JSONArray list=JSONArray.parseArray(authority);
for (int i=0;i<list.size();i++){
JSONObject jsonObject=list.getJSONObject(i);
authorities.add(new MyGrantedAuthority(jsonObject.getString("path"),jsonObject.getString("method")));
}
}
if (StringUtils.isNotBlank(username)) {
//此处password不能为null
User principal = new User(username, "", authorities);
return new UsernamePasswordAuthenticationToken(principal, null, authorities);
}
} catch (ExpiredJwtException e) {
throw new CommonException(ResultCode.BAD_REQUEST, "登录已失效,请重新登录");
} catch (Exception e) {
throw new CommonException(ResultCode.BAD_REQUEST, "解析token错误");
}
}
return null;
}
}
  • 第二判断当前请求token是否有权访问当前请求地址
    MyFilterSecurityInterceptor
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
package com.lhm.springcloud.security.filter;

import com.lhm.springcloud.security.manager.MyAccessDecisionManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.SecurityMetadataSource;
import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
import org.springframework.security.access.intercept.InterceptorStatusToken;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import java.io.IOException;

/**
* 权限管理过滤器2
* 监控用户行为
* @author Exrickx
*/

@Component
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {

@Autowired
private FilterInvocationSecurityMetadataSource securityMetadataSource;

@Autowired
public void setMyAccessDecisionManager(MyAccessDecisionManager myAccessDecisionManager) {
super.setAccessDecisionManager(myAccessDecisionManager);
}

@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
}
//fi里面有一个被拦截的url
//里面调用MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法获取fi对应的所有权限
//再调用MyAccessDecisionManager的decide方法来校验用户的权限是否足够
public void invoke(FilterInvocation fi) throws IOException, ServletException {

InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.afterInvocation(token, null);
}
}

@Override
public void destroy() {

}

@Override
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}

@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.securityMetadataSource;
}
}

具体的处理会放到MySecurityMetadataSource中去判断,不过我这里做了个小优化,将处理权限的业务统一放到了MyAccessDecisionManager下,减少点性能开销

MySecurityMetadataSource

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
package com.lhm.springcloud.security.manager;

import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.Collection;

/**
* 权限资源管理器
* 为权限决断器提供支持
*
* @author Exrickx
*/

@Component
public class MySecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
/**
* 此方法是为了判定用户请求的url 是否在权限表中,如果在权限表中,则返回给 decide 方法,用来判定用户是否有此权限。如果不在权限表中则放行。
* 因为每一次来了请求,都先要匹配一下权限表中的信息是不是包含此url,
* 因此优化一下,对url直接拦截,不管请求的url 是什么都直接拦截,然后在MyAccessDecisionManager的decide 方法中做拦截还是放行的决策。
* 所以此方法的返回值不能返回 null 此处随便返回一下。
*
* @param o
* @return
* @throws IllegalArgumentException
*/
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {

Collection<ConfigAttribute> co = new ArrayList<>();
co.add(new SecurityConfig("null"));
return co;
}

@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}

@Override
public boolean supports(Class<?> aClass) {
return true;
}
}

MyAccessDecisionManager

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
package com.lhm.springcloud.security.manager;

import com.lhm.springcloud.security.pojo.MyGrantedAuthority;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import java.util.Collection;

/**
* @ClassName MyAccessDecisionManager
* @Description 权限最终判断器
* * 判断用户拥有的角色是否有资源访问权限
* @Author Alan
* @Date 2019/5/7 10:44
* @Version 1.0
**/
@Service
public class MyAccessDecisionManager implements AccessDecisionManager {

//decide 方法是判定是否拥有权限的决策方法
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
String url, method;
AntPathRequestMatcher matcher;
for (GrantedAuthority ga : authentication.getAuthorities()) {
if (ga instanceof MyGrantedAuthority) {
MyGrantedAuthority urlGrantedAuthority = (MyGrantedAuthority) ga;
url = urlGrantedAuthority.getPermissionUrl();
method = urlGrantedAuthority.getMethod();
matcher = new AntPathRequestMatcher(url);
if (matcher.matches(request)) {
//当权限表权限的method为ALL时表示拥有此路径的所有请求方式权利。
if (method.equals(request.getMethod()) || "ALL".equals(method)) {
return;
}
}
}
throw new AccessDeniedException("您没有访问权限");
}
throw new AccessDeniedException("鉴权出错");
}



@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}

@Override
public boolean supports(Class<?> clazz) {
return true;
}
}

decide()方法中的MyGrantedAuthority是我自定义的权限对象 因为原有的SimpleGrantedAuthority类只有一个属性,无法完成RESTfull风格的请求。
MyGrantedAuthority

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
package com.lhm.springcloud.security.pojo;

import org.springframework.security.core.GrantedAuthority;

/**
* @ClassName MyGrantedAuthority
* @Author Alan
* @Date 2018/5/7 10:39
* @Version 1.0
**/
public class MyGrantedAuthority implements GrantedAuthority {
private String url;
private String method;

public String getPermissionUrl() {
return url;
}

public void setPermissionUrl(String permissionUrl) {
this.url = permissionUrl;
}

public String getMethod() {
return method;
}

public void setMethod(String method) {
this.method = method;
}

public MyGrantedAuthority(String url, String method) {
this.url = url;
this.method = method;
}

@Override
public String getAuthority() {
return this.url + ";" + this.method;
}
}

配置

最后将我们自定义的类全部注入到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
package com.lhm.springcloud.security.config;

import com.lhm.springcloud.security.constant.IgnoredUrlsProperties;
import com.lhm.springcloud.security.filter.JWTAuthenticationFilter;
import com.lhm.springcloud.security.filter.MyFilterSecurityInterceptor;
import com.lhm.springcloud.security.filter.WebSecurityCorsFilter;
import com.lhm.springcloud.security.handler.RestAccessDeniedHandler;
import com.lhm.springcloud.security.service.impl.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.access.channel.ChannelProcessingFilter;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

/*
* Security 核心配置类
* 开启控制权限至Controller
* @author Exrickx
* */


@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private IgnoredUrlsProperties ignoredUrlsProperties;

@Autowired
private UserDetailsServiceImpl userDetailsService;

@Autowired
private AuthenticationSuccessHandler successHandler;

@Autowired
private AuthenticationFailureHandler failHandler;

@Autowired
private RestAccessDeniedHandler accessDeniedHandler;

@Autowired
private MyFilterSecurityInterceptor myFilterSecurityInterceptor;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
//密码加密使用 Spring Security 提供的BCryptPasswordEncoder.encode(user.getRawPassword().trim())
}

@Override
protected void configure(HttpSecurity http) throws Exception {
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http
.authorizeRequests();
//除配置文件忽略路径其它所有请求都需经过认证和授权
for(String url:ignoredUrlsProperties.getUrls()){
registry.antMatchers(url).permitAll();
}
registry.antMatchers(HttpMethod.OPTIONS).permitAll()
.and()
//表单登录方式
.formLogin()
.loginPage("/login/needLogin")
//登录需要经过的url请求
.loginProcessingUrl("/api/v1/auth/login")
.usernameParameter("username")
.passwordParameter("password")
.permitAll()
//成功处理类
.successHandler(successHandler)
//失败
.failureHandler(failHandler)
.and()
.logout()
.permitAll()
.and()
.authorizeRequests()
//任何请求
.anyRequest()
//需要身份认证
.authenticated()
.and()
//关闭跨站请求防护
.csrf().disable()
//前后端分离采用JWT 不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
//自定义权限拒绝处理类
.exceptionHandling().accessDeniedHandler(accessDeniedHandler)
.and()
//添加自定义权限过滤器
.addFilterBefore(new WebSecurityCorsFilter(), ChannelProcessingFilter.class)
.addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class)
//添加JWT过滤器 除/login其它请求都需经过此过滤器
.addFilter(new JWTAuthenticationFilter(authenticationManager()));
}
}