Saul's blog Saul's blog
首页
后端
分布式
前端
更多
分类
标签
归档
友情链接
关于
GitHub (opens new window)

Saul.J.Wu

立身之本,不在高低。
首页
后端
分布式
前端
更多
分类
标签
归档
友情链接
关于
GitHub (opens new window)
  • 安全框架(认证、授权)

    • 分布式系统认证方案
    • OAuth2介绍
    • SpringCloudSecurityOAuth2
      • 简介
        • 1、用户认证
        • 2、用户授权
      • 认证与授权原理
      • 整合
        • 引入依赖
        • 创建数据库
      • 配置SpringSecurity
        • UserDetailsService
        • WebSecurityConfig
      • JWT
        • jwt概念
        • 传统token
        • jwt组成部分
        • 请问base64术语加密算法吗?
        • Jwt与Token之间的区别
        • jwt应用场景
        • Jwt优缺点
        • 手写jwt模拟base64
        • Java与jwt
      • SpringSecurity整合jwt
        • 登陆流程
        • 鉴权
        • jwt工具类
        • 登陆成功返回token
        • 请求拦截token
        • 配置过滤器
        • jwt如何确保安全?
        • jwt如何注销账户?
        • Jwt如何绑定userId?
      • SpringSecurity整合oauth2.0
        • 认证授权中心
        • 资源服务器端
      • 安全架构设计方案
        • 如何防御xss攻击
    • 单点登陆简介
    • 认证中心整合
  • 分布式事务

  • 消息队列

  • K8S

  • 分布式
  • 安全框架(认证、授权)
SaulJWu
2021-02-20

SpringCloudSecurityOAuth2

# 简介

SpringSecurity主要包含两部分:用户认证与用户授权。

Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。

摘自百度百科:

[https://baike.baidu.com/item/spring%20security/8831652?fr=aladdin](https://baike.baidu.com/item/spring security/8831652?fr=aladdin)

# 1、用户认证

用户进行登录时候,输入用户名和密码(或者短信动态验证登陆),查询数据库,输入用户名和密码是否正确,如果正确的话,认证就成功了。

认证就是否登陆的意思。

# 2、用户授权

登陆了系统,登陆用户可能是不同的角色,比如现在登陆的用户是管理员,管理员操作所有功能,比如登陆用户为普通用户,操作功能肯定比管理员少很多。

SpringSecurity本质上就是一个过滤器,对我们的请求进行过滤。

  1. 如果是基于Session,那么SpringSecurity会对cookie的sessionid进行解析,汇总爱到服务器存储的session信息,然后判断当前用户是否符合请求的要求。
  2. 如果是基于tokn,则是解析出token,然后将当前请求加入到SpringSecurity管理的权限信息中去。

# 认证与授权原理

如果系统的模块众多,每个模块都需要进行授权与认证,所以其我们选择基于token的形式进行授权与认证,用户根据用户名密码认证成功,然后获取当前用户角色的一些列权限值,并以用户名为key,权限列表为value的形式存入redis缓存中,根据用户名相关信息生成token返回,前端每次调用api即可将token携带到header请求头中,Springsecurity解析header头获取token信息,解析token获取当前用户名,根据用户名就可以从redis中获取权限列表,这样Springsecurity就能够判断当前请求是否有权限访问。

# 整合

# 引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
1
2
3
4

# 创建数据库

一般权限控制有三层,即:用户<–>角色<–>权限,用户与角色是多对多,角色和权限也是多对多。

所以需要5个表:

  • 用户表
  • 角色表
  • 权限表
  • 用户与角色关系表
  • 角色与权限关系表

# 创建用户表

CREATE TABLE `sys_user` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  `password` varchar(255) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
1
2
3
4
5
6

# 创建角色表

CREATE TABLE `sys_role` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
1
2
3
4
5

# 创建权限表

CREATE TABLE `sys_permission` (
  `id` bigint(0) NOT NULL AUTO_INCREMENT,
  `parent_id` bigint(0) NULL DEFAULT NULL COMMENT '父权限资源ID,顶级权限资源id为0',
  `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '权限名称',
  `url` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '权限URL',
  `perms` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '授权(多个用逗号分隔,如:user:list,user:create)',
  `type` int(0) NULL DEFAULT NULL COMMENT '权限类型   0:目录   1:菜单   2:按钮',
  `icon` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '权限图标',
  `weight` int(0) NULL DEFAULT NULL COMMENT '权重',
  PRIMARY KEY (`menu_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 126 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '权限资源管理' ROW_FORMAT = Dynamic;
1
2
3
4
5
6
7
8
9
10
11

注意:这里的权限格式为ROLE_XXX,是Spring Security规定的,不要乱起名字。

# 创建用户与橘色关系表

CREATE TABLE `sys_user_role`  (
  `id` bigint(0) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(0) NULL DEFAULT NULL COMMENT '用户ID',
  `role_id` bigint(0) NULL DEFAULT NULL COMMENT '角色ID',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户与角色对应关系' ROW_FORMAT = Dynamic;
1
2
3
4
5
6

# 创建角色与权限关系表

CREATE TABLE `sys_role_permission`  (
  `id` bigint(0) NOT NULL AUTO_INCREMENT,
  `role_id` bigint(0) NULL DEFAULT NULL COMMENT '角色ID',
  `permission_id` bigint(0) NULL DEFAULT NULL COMMENT '权限ID',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '角色与权限对应关系' ROW_FORMAT = Dynamic;
1
2
3
4
5
6

# 配置SpringSecurity

# UserDetailsService

首先我们需要自定义 UserDetailsService,将用户信息和权限注入进来。

我们需要重写 loadUserByUsername方法,参数是用户输入的用户名。返回值是UserDetails,这是一个接口,一般使用它的子类org.springframework.security.core.userdetails.User,它有三个参数,分别是用户名、密码和权限集。

@Service("userDetailsService")
public class CustomUserDetailsService implements UserDetailsService {
    @Autowired
    private SysUserService userService;

    @Autowired
    private SysRoleService roleService;

    @Autowired
    private SysUserRoleService userRoleService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        // 从数据库中取出用户信息 redis或者mysql自行编写
        SysUser user = userService.selectByName(username);

        // 判断用户是否存在
        if(user == null) {
            throw new UsernameNotFoundException("用户名不存在");
        }
        //判断用户是否禁用锁定自行编写

        // 添加权限
        List<SysUserRole> userRoles = userRoleService.listByUserId(user.getId());
        for (SysUserRole userRole : userRoles) {
            SysRole role = roleService.selectById(userRole.getRoleId());
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }

        // 返回UserDetails实现类
        return new User(user.getName(), user.getPassword(), authorities);
    }
}
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

# WebSecurityConfig

该类是 Spring Security 的配置类,该类的三个注解分别是标识该类是配置类、开启 Security服务、开启全局 Securtiy注解。

首先将我们自定义的 userDetailsService注入进来,在configure()方法中使用 auth.userDetailsService()方法替换掉默认的 userDetailsService。

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private CustomUserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(new PasswordEncoder() {
            @Override
            public String encode(CharSequence charSequence) {
                return charSequence.toString();
            }

            @Override
            public boolean matches(CharSequence charSequence, String s) {
                return s.equals(charSequence.toString());
            }
        });
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                // 如果有允许匿名的url,填在下面
//                .antMatchers().permitAll()
                .anyRequest().authenticated()
                .and()
                // 前后端分离,不需要设置登陆页
                //.formLogin().loginPage("/login")
                // 设置登陆成功页
                .defaultSuccessUrl("/").permitAll()
                // 自定义登陆用户名和密码参数,默认为username和password
//                .usernameParameter("username")
//                .passwordParameter("password")
                .and()
                .logout().permitAll();

        // 关闭CSRF跨域
        http.csrf().disable();
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        // 设置拦截忽略文件夹,可以对静态资源放行 swagger这些自行添加
        web.ignoring().antMatchers("/css/**", "/js/**");
    }
}
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

# JWT

# jwt概念

Jwt JSON WEB Token JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

# 传统token

传统的Token,例如:用户登录成功生成对应的令牌,key为令牌 value:userid,隐藏了数据真实性 ,同时将该token存放到redis中,返回对应的真实令牌给客户端存放。

客户端每次访问后端请求的时候,会传递该token在请求中,服务器端接收到该token之后,从redis中查询如果存在的情况下,则说明在有效期内,如果在Redis中不存在的情况下,则说明过期或者token错误。

# jwt组成部分

JWT,其实是json web token的缩写,它由三部分组成,每一个部分都用.隔开。

官网:https://jwt.io/

image.png

第一步部分是header,是jwt的加密方式。

{
 Typ=”jwt”  ---类型为jwt
 Alg:”HS256”  --加密算法为hs256
}
1
2
3
4

第二部分是Payload(载荷):实际就是jwt存放的数据, 用户名称、用户头像之类,需要注意敏感数据。

标准中注册的声明 (建议但不强制使用) :

iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
1
2
3
4
5
6
7
8
9

第三方部分是sign(签名值),因为Payload数据其实还明文的,有了验证签名,主要防止抓包串改数据,采用MD5,注意,签名值不是秘钥,它只是一个盐值,网上很多文章说这是秘钥,那是错误的。

JWT=Base64.Encoded(Header+Payload).签名值

# 请问base64术语加密算法吗?

Base64不是加密和解密 主要是 编码和解码 基于64个可打印字符来表示二进制数据,其实它就是一个编码器,并没有对数据进行加密或者解密,所以要注意晚上很多文章都说base64是加密是错误的。

另外,在网络程序员交流联系方式时,可以用base64编码一下,提高获取真实练习方式门槛。

https://baike.baidu.com/item/base64/8545775?fr=aladdin

#

# Jwt与Token之间的区别

  1. token对应存放的数据放在redis中
  2. Jwt对应存放的数据(Payload中)客户端,如果没有签名是无法篡改

#

# jwt应用场景

前后分离(移动App项目、企业管理平台Vue项目、小程序问题)

#

# Jwt优缺点

# 优点

  1. Jwt数据存放客户端,不依赖于服务器端,所以可以减轻服务器端压力。
  2. 轻量级、json风格比较简单,效率比传统的token验证还要高,传统的token验证需要查redis,效率相对较低。
  3. 跨语言

# 缺点

  1. jwt一旦生成之后无法修改,要想新的值数据,只能重新生成。

  2. 无法销毁一个jwt,很难做注销

    1. 就算是客户端销毁了jwt,只要有心人保留这个jwt还是可以发送请求

# 手写jwt模拟base64

# 加密

jwt由3个部分组成,所以我们需要封装三个部分header、payload、sign(签名值)

public class Test{
    
    private static fianl String SIGN_KEY = "yourSignKey";
    
    public static void main(String[] args){
        //header
        JSONObject header = new JSONObject();
        header.put("alg","HS256");
        //payload
        JSONObject payload = new JSONObject();
        payload.put("phone","133****1234");
        //sign签名值 实际上就是md5      
        String sign = DigestUtils.md5Hex(payload.toJSONString() + SIGN_KEY);
        //封装为jwt
        String headerEncoded  = Base64.getEncoder().encodeToString(header.toJSONString().getBytes());
        String payLoadENcoded = Base64.getEncoder().encodeToString(payload.toJSONString().getBytes());
        String jwt = headerEncoded + "." + payLoadENcoded + "." + sign;
        System.out.println(jwt);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

最后这个打印出来的值,拿去jwt官网,也是可以解析的,说明成功。

# 解密

public class Test02{
    
    private static fianl String SIGN_KEY = "yourSignKey";
    
    public static void main(String[] args){
        String jwt = "";
        String payLoadStr = jwt.split("\\.")[1];
        String payLOadDecoder = new String(Base64.getDecoder().decode(payLoadEncode),"UTF-8");
        String newSign = DigestUtils.md5Hex(payLoadDecoder + SIGN_KEY);
        System.out.println(newSign.equals(jwt.split("\\.")[2]));
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

如果打印出来的是true,说明jwt没有被篡改。

#

# Java与jwt

前面我们学会了手写jwt,那么接下来常规项目开发中JWT加密和解密也很简单。

# maven依赖

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.6.0</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.62</version>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 加密

// 创建jwt
JwtBuilder jwtBuilder = Jwts.builder().setId("66").setSubject("mayikt")
        .setIssuedAt(new Date())
        // 设置签名值
        .signWith(SignatureAlgorithm.HS256, signKey);
System.out.println(jwtBuilder.compact());


// 创建jwt
JwtBuilder jwtBuilder = Jwts.builder()
        .setIssuedAt(new Date())
        .claim("userId","644064")
        // 设置签名值
        .signWith(SignatureAlgorithm.HS256, signKey);
System.out.println(jwtBuilder.compact());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 解密

所谓解密就是验证的意思,相当于解析。

private static final String signKey = "mayiktSign";
public static void main(String[] args) {
    String jwt = "eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1OTE0MjY4MjEsInVzZXJJZCI6IjY0NDA2NCJ9.86LauCphjTZrQdmcRVuM7Nr4IpJDwdkKI5k2cjg91rQ";
    Claims body = Jwts.parser().setSigningKey(signKey).parseClaimsJws(jwt).getBody();
    System.out.println(body.get("userId"));
}
1
2
3
4
5
6

# jwt过期

private static final String signKey = "mayiktSign";

public static void main(String[] args) {
    //当前时间
    long now = System.currentTimeMillis();
    //过期时间为1分钟
    long exp = now + 1000 * 5;
    JwtBuilder builder = Jwts.builder()
            .setIssuedAt(new Date())
            .claim("userId", "1234")
            .signWith(SignatureAlgorithm.HS256, signKey)
            .setExpiration(new Date(exp));//用于设置过期时间
    System.out.println(builder.compact());

    try {
        Thread.sleep(1000);
    } catch (Exception e) {

    }
    Claims body = Jwts.parser().setSigningKey(signKey).parseClaimsJws(builder.compact()
    ).getBody();
    System.out.println(body);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# SpringSecurity整合jwt

在了解了这么多jwt的知识点后,我们言归正传,将它整合到SpringSecurity中。这样就不用频繁查询数据库了,直接通过jwt就可以知道是否有权限。

# 登陆流程

image.png

  1. 验证账号密码 UserDetailsService
  2. 账号密码如果验证成功下,生成jwt

Jwt PayLoad中存放哪些内容?

userId? 不可以放,它是敏感数据。

avatar?头像,可以放。

username?如果不是敏感数据可以放

permissionList?权限列表,可以放

  1. 返回jwt给客户端(移动app、浏览器、微信小程序等)

# 鉴权

1、base64解密jwt,获取payload中的数据

2、获取permissionList权限列表,注册到Springsecurity框架中

3、验证是否有权限,决定是否放行请求。

# jwt工具类

public class MayiktJwtUtils {

    public static final String TOKEN_HEADER = "token";
    public static final String TOKEN_PREFIX = "Bearer ";

    private static final String SUBJECT = "mayikt";

    private static final long EXPIRITION = 1000 * 24 * 60 * 60 * 7;

    private static final String APPSECRET_KEY = "mayikt_secret";

    private static final String PERMISSION_CLAIMS = "permissionList";

    public static String generateJsonWebToken(UserEntity user) {
        String token = Jwts
                .builder()
                .setSubject(SUBJECT)
                .claim(PERMISSION_CLAIMS, user.getAuthorities())
                .claim("id", user.getId())
                .claim("username", user.getUsername())
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRITION))
                .signWith(SignatureAlgorithm.HS256, APPSECRET_KEY).compact();
        return token;
    }

    /**
     * 生成token
     *
     * @param username
     * @param role
     * @return
     */
    public static String createToken(String username, String role) {

        Map<String, Object> map = new HashMap<>();
        map.put(PERMISSION_CLAIMS, role);

        String token = Jwts
                .builder()
                .setSubject(username)
                .setClaims(map)
                .claim("username", username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRITION))
                .signWith(SignatureAlgorithm.HS256, APPSECRET_KEY).compact();
        return token;
    }

    public static Claims checkJWT(String token) {
        try {
            final Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
            return claims;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 获取用户名
     *
     * @param token
     * @return
     */
    public static String getUsername(String token) {
        Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
        return claims.get("username").toString();
    }

    /**
     * 获取用户权限
     *
     * @param token
     * @return
     */
    public static List<SimpleGrantedAuthority> getUserPermissionList(String token) {
        Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
        List roles = (List) claims.get(PERMISSION_CLAIMS);
        String json = JSONArray.toJSONString(roles);
        List<SimpleGrantedAuthority>
                grantedAuthorityList =
                JSONArray.parseArray(json, SimpleGrantedAuthority.class);

        return grantedAuthorityList;
    }

    /**
     * 是否过期
     *
     * @param token
     * @return
     */
    public static boolean isExpiration(String token) {
        Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
        return claims.getExpiration().before(new Date());
    }

}
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

# 登陆成功返回token

public class JWTLoginFilter extends UsernamePasswordAuthenticationFilter {
    /**
     * 获取授权管理
     */
    private AuthenticationManager authenticationManager;

    public JWTLoginFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
        /**
         *  后端登陆接口
         */
        super.setFilterProcessesUrl("/auth/login");
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest req,
                                                HttpServletResponse res) {
        try {
            UserEntity user = new ObjectMapper()
                    .readValue(req.getInputStream(), UserEntity.class);

            return authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(
                            user.getUsername(),
                            user.getPassword(),
                            new ArrayList<>())
            );
        } catch (IOException e) {
            logger.error(e.getMessage());
            return  null;
        }
    }

    @Override
    /**
     * 用户登陆成功之后验证
     */
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
                                            Authentication authResult) throws IOException, ServletException {
        UserEntity userEntity = (UserEntity) authResult.getPrincipal();
        String jwtToken = MayiktJwtUtils.generateJsonWebToken(userEntity);
        response.addHeader("token", jwtToken);
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");
        response.getWriter().print("账号或者密码错误");
    }
}
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

# 请求拦截token

public class JWTValidationFilter extends BasicAuthenticationFilter {
    public JWTValidationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    /**
     * 过滤请求验证
     *
     * @param request
     * @param response
     * @param chain
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        SecurityContextHolder.getContext().setAuthentication(setAuthentication(request.getHeader("token")));
        super.doFilterInternal(request, response, chain);
    }

    /**
     * 验证token 并且验证权限
     * @param token
     * @return
     */
    private UsernamePasswordAuthenticationToken setAuthentication(String token) {
        String username = MayiktJwtUtils.getUsername(token);
        if (username == null) {
            return null;
        }
        List<SimpleGrantedAuthority> permissionList = MayiktJwtUtils.getUserPermissionList(token);
        return new UsernamePasswordAuthenticationToken(username, null, permissionList);
    }
}
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

# 配置过滤器

@Override
protected void configure(HttpSecurity http) throws Exception {
    List<PermissionEntity> allPermission = permissionMapper.findAllPermission();
    ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry
            expressionInterceptUrlRegistry = http.authorizeRequests();
    allPermission.forEach((permission) -> {
        expressionInterceptUrlRegistry.antMatchers(permission.getUrl()).
                hasAnyAuthority(permission.getPermTag());

    });
    // 配置前后令牌登陆
    expressionInterceptUrlRegistry.antMatchers("/auth/login").permitAll()
            .antMatchers("/**").fullyAuthenticated()
            .and()
            .addFilter(new JWTValidationFilter(authenticationManager()))
            .addFilter(new JWTLoginFilter(authenticationManager())).csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# jwt如何确保安全?

jwt保存在客户端的payload中,如果想要篡改,必须获取到生产服务器上的sign值,如果没有,是无法篡改的,所以jwt是安全的。

# jwt如何注销账户?

jwt无法做真正意义上注销

  1. 用户注销的时候,直接将cookie缓存清除。
  2. 建议最好将jwt过期时间定义不要太长
  3. 每个用户对应的盐值不一样,用户在注销的时候直接将该盐值发生变化,就算后面有人再拿到jwt,盐值已经发生变化,肯定解析不正确.

当然如果你真的需要实现控制注销,也可以在redis服务器上存一份jwt,但是这样就失去了jwt的高效率意义,所以jwt的缺点还是挺明显的,当然jwt还是利大于弊的。

# Jwt如何绑定userId?

如果非要在jwt中payload中存放userId,可以单独对userId实现对称加密,几乎很难破解,因为userId加密解密都是放在服务器端处理,但是这样效率也变低了。所以一般不建议在payload中存放userId,这样黑客的攻击机会就少了。

# SpringSecurity整合oauth2.0

oauth2.0 不是一门技术,而是一种协议,开放授权协议,是一种思想,在很多大公司,oauth都是自己实现的,并不完全依赖于springSecurity,每个公司都有自己的流程。

image.png

# 认证授权中心

# maven依赖

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.1.RELEASE</version>
</parent>


<dependencies>
    <!-- SpringBoot整合Web组件 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>

    <!-- springboot整合freemarker -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-freemarker</artifactId>
    </dependency>

    <!-->spring-boot 整合security -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    <!-- Spring Security OAuth2 -->
    <dependency>
        <groupId>org.springframework.security.oauth</groupId>
        <artifactId>spring-security-oauth2</artifactId>
        <version>2.2.1.RELEASE</version>
    </dependency>


    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.6.0</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.62</version>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
    </dependency>
</dependencies>
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

# 配置类

@Component
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    //实际上这里应该查库
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.
                inMemoryAuthentication()
                .withUser("mayikt")
                .password(passwordEncoder().encode("123456"))
                .authorities("/*");


    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated() //所有请求都需要通过认证
                .and()
                .httpBasic() //Basic登录
                .and()
                .csrf().disable(); //关跨域保护
    }
}

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
@Component
@EnableAuthorizationServer
public class AuthorizationConfig extends AuthorizationServerConfigurerAdapter {
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        //允许表单提交
        security.allowFormAuthenticationForClients()
                .checkTokenAccess("permitAll()");
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                // appid
                .withClient("mayikt")
                // appsecret
                .secret(passwordEncoder.encode("mayikt_secret"))
                // 授权码
                .authorizedGrantTypes("authorization_code")
                // 作用域 表示所有的接口都可以访问,分配我们的appid 调用接口的权限
                .scopes("all")
                // 资源的id
                .resourceIds("mayikt_resource")
                // 回调地址
                .redirectUris("http://www.mayikt.com/callback");

    }
}
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

# 资源服务器端

# 相关配置类

/**
 * 资源Server端
 */
@Configuration
@EnableResourceServer
public class ResourceConfig extends ResourceServerConfigurerAdapter {

    @Value("${mayikt.appid}")
    private String mayiktAppId;
    @Value("${mayikt.appsecret}")
    private String mayiktAppSecret;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Primary
    @Bean
    public RemoteTokenServices remoteTokenServices() {
        final RemoteTokenServices tokenServices = new RemoteTokenServices();
        //设置授权服务器check_token端点完整地址
        tokenServices.setCheckTokenEndpointUrl("http://localhost:8080/oauth/check_token");
        //设置客户端id与secret,注意:client_secret值不能使用passwordEncoder加密!
        tokenServices.setClientId(mayiktAppId);
        tokenServices.setClientSecret(mayiktAppSecret);
        return tokenServices;
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        //设置创建session策略
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
        //@formatter:off
        //所有请求必须授权
        http.authorizeRequests()
                .anyRequest().authenticated();
        //@formatter:on
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.resourceId("mayikt_resource").stateless(true);
    }
}
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

# 安全架构设计方案

  1. 使用api网关过滤器对防止xss、sql注入问题 # $

# 可以预编译防止sql注入

<script>alert(‘ss’)</script> or 1=1 
1
  1. 对我们的接口实现对称加密,抓包的时候看不到明文的数据,但是可以被破解,因为客户端与服务器都是采用同一个密钥实现加解密,可以通过反编译客户端代码得出密钥实现破解。Des、aes

  2. 使用非对称加密RSA 公钥和私钥互换机制,客户端使用公钥实现加密,服务器端采用私钥实现解密,就算黑客破解出公钥也无法对数据实现解密。

一对密钥 两个密钥公钥 和私钥

对称加解密速度比rsa要快,但是不安全。

  1. 可以采用MD5对我们的参数实现验证签名,但是数据还是传输明文,可以防止

篡改数据。

  1. 结合jwt传递传递参数,数据不需要存放在服务器端,可以减少服务器端的压力

  2. 互联网电商项目都会采用Https协议 ssl+证书 加密传输 默认443 而我们的Http协议采用明文实现对数据传输,效率比较Https要高,但是不安全。

  3. 对我们的api接口实现黑名单和白名单控制nginx、网关

  4. 对我们接口实现服务保护、限流、熔断

  5. 使用图形验证码防止机器模拟请求

  6. 对我们代码使用专门扫描工具实现检测漏洞

  7. 在rpc传递参数过程中使用令牌隐藏真实的参数。 token value放入redis

  8. 数据库表rbdac权限框架设计对不同角色实现权限控制

  9. 可以构建企业级oauth2.0协议保证开放接口的安全性

  10. 基于nginx防御ddos攻击 集合lua代码

# 如何防御xss攻击

参数过滤即可

@Component
@WebFilter
public class XssFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        XssHttpServletRequestWrapper xssHttpServletRequestWrapper = new XssHttpServletRequestWrapper((HttpServletRequest) request);
        chain.doFilter(xssHttpServletRequestWrapper, response);
    }


    @Override
    public void destroy() {

    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {

    private HttpServletRequest request;

    public XssHttpServletRequestWrapper(HttpServletRequest request) {
        super(request);
        this.request = request;
    }

    @Override
    public String getParameter(String name) {
        String oldValue = super.getParameter(name);
        if(StringUtils.isEmpty(oldValue)){
            return oldValue;
        }
        String newValue = StringEscapeUtils.escapeHtml4(oldValue);
        return newValue;
    }
    
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
帮我改善此页面 (opens new window)
上次更新: 2021/07/06, 02:30:10
OAuth2介绍
单点登陆简介

← OAuth2介绍 单点登陆简介→

最近更新
01
zabbix学习笔记二
02-28
02
zabbix学习笔记一
02-10
03
Linux访问不了github
12-08
更多文章>
Theme by Vdoing | Copyright © 2020-2022 Saul.J.Wu | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式