JWT 和 Spring Security 保护 REST API(1)

       通常情况下,把API直接暴露出去是风险很大的,不说别的,直接被机器攻击就够喝一壶的。那么一般来说,对API要划分出一定的权限级别,然后做一个用户的鉴权,依据鉴权结果给予用户开放对应的API。目前,比较主流的方案有几种:

1.用户名和密码鉴权,使用Session保存用户鉴权结果。
2.使用OAuth进行鉴权(其实OAuth也是一种基于Token的鉴权,只是没有规定Token的生成方式)

3.自行采用Token进行鉴权

第一种就不介绍了,由于依赖Session来维护状态,也不太适合移动时代(不是网站浏览器,保存session不太方便)。有人会说那么用session共享方式呢(如redisSession)?这种方式对保存session服务器的要求非常高,如果这个服务器出问题,则大家就访问不了了。所以用session保存用户鉴权的结果又叫集中式保存用户鉴权结果的方案。

第二种OAuth的方案和JWT都是基于Token的(分布式保存用户鉴权结果的方案),但OAuth其实对于不做开放平台的公司有些过于复杂。

所以我们采用第三种来玩玩。

那么什么是JWT?

JWT是 Json Web Token 的缩写。它是基于 RFC 7519 标准定义的一种可以安全传输的 小巧 和 自包含 的JSON对象。由于数据是使用数字签名的,所以是可信任的和安全的。JWT可以使用HMAC算法对secret进行加密或者使用RSA的公钥私钥对来进行签名。

JWT的工作流程

下面是一个JWT的工作流程图。模拟一下实际的流程是这样的(假设受保护的API在/protected中)

1.用户导航到登录页,输入用户名、密码,进行登录
2.服务器验证登录鉴权,如果用户合法,根据用户的信息和服务器的规则生成JWT Token
3.服务器将该token以json形式返回(不一定要json形式,这里说的是一种常见的做法)
4.用户得到token,存在localStorage、cookie或其它数据存储形式中。
5.以后用户请求/protected中的API时,在请求的header中加入 Authorization: Bearer xxxx(token)。此处注意token之前有一个7字符长度的 Bearer
6.服务器端对此token进行检验,如果合法就解析其中内容,根据其拥有的权限和自己的业务逻辑给出对应的响应结果。

7.用户取得结果





       JWT只是我们方案中的一部分,除了JWT,我们还要使用Spring Security,它是spring框架中提供的专门基于安全权限管理的框架。所以,我们使用JWT+SpringSecurity的方案来保护REST API。

       SpringSecurity主要是用在服务端。服务端配这个框架来保证哪些API是哪种权限的用户可以访问的。

       JWT是送给客户端保存的内容。当然,JWT本身的生成是在服务端,生成了后就要送给客户端本地保存。


好,基本的概念都完成了额,Just do it !

-------------------------------------------------Spring Security---------------------------------------------------------

我们首先配置SpringSecurity。配置之前,我要提前说下,如果我们的系统有用户登录的操作的话,会有一张用户表是吧?现在,除了这张用户表,我们还要有一个角色表和用户角色关联表(用户角色是多对多的关系的)。因为SpringSecurity是基于角色在控制用户的权限,(所以用这个框架是必须要这三张表的)




有了这三张表之后,我们要去pom.xml中创建依赖了(jar包)



然后再配置文件application.yml中添加红线一行(对jackson序列化输出的配置和日志的输出级别)



我们先来看下目录,看看哪些类与SpringSecurity框架有关



首先我们要在domain包里面定义枚举类AuthorityName,它是根据角色表authority来建立的


从上面authority表可以看出有两个角色:ROLE_ADMIN 和 ROLE_USER 。所以枚举类AuthorityName的代码如下(枚举类是用来定义一组固定值的,等于固定两种角色)

package com.yy.hospital.domain;

public enum AuthorityName {
    ROLE_ADMIN,ROLE_USER
}

然后时domain里面的角色类Authority(角色的pojo表只定义了id和name,其他的属性后面需要再加)

package com.yy.hospital.domain;

public class Authority {

    private Integer id;
    private AuthorityName name;  //角色名是必须限定好的枚举

    //在这里,getter和setter方法省略
}

最后就是domain包里面的Admin类

package com.yy.hospital.domain;

import com.fasterxml.jackson.annotation.JsonFormat;

import java.io.Serializable;
import java.util.Date;
import java.util.List;

public class Admins implements Serializable{
    private Integer aid;
    private String aname;
    private String pwd;
    private Integer state;
    private String email;
    private Date lastPasswordResetDate;
    private  Integer aexist;
    @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
    private Date loginTime;
    private Integer doid;
    private String by1;

    //用户的角色集合。用户登录后就可以获得它自己的权限(权限是查出来的)
    private List<Authority> authorities;

	
	//这里,getter和setter省略
}

注意,Admin类里面有一个角色集合List<Authority> authorities,所以当用户登录进来后,用户的角色信息也要添加到属性authorities里面,这里就涉及到多表查询了,后面大家就会看到。


好,现在要添加核心类了,首先建立个security包,这里面放所有安全配置相关的类

首先我们建立用户的服务对象(即SpringSecurity框架所要使用的用户)。我们取名JwtUser。这个JwtUser里面的信息是最终要生成jwt令牌里面的user信息。SpringSecurity框架是要基于用户类型来提供服务,这个用户类型是要实现了SpringSecurity框架里面的UserDetails接口的类;换句话来说,也就是实现UserDetails接口的用户类(JwtUser),这个用户就是SpringSecurity框架提供服务的用户类

package com.yy.hospital.security.domain;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.yy.hospital.domain.Authority;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.Date;
import java.util.List;

/**
 * spring security框架服务的用户类
 */
public class JwtUser implements UserDetails{

    private final Integer id;           // 必须
    private final String username;     // 必须
    private final String password;     // 必须
    private final Integer state;
    private final String email;
    private final Date lastPasswordResetDate;
    private final boolean enabled;    // 必须    //表示当前这个用户是否可以使用,替换了aexist
    private final Date loginTime;

    //授权的角色集合---不是用户的角色集合
    //权限的类型要继承GrantedAuthority
    private final Collection<? extends GrantedAuthority> authorities;       // 必须

    public JwtUser(Integer id, String username, String password, Integer state, String email, Date lastPasswordResetDate, boolean enabled, Date loginTime, Collection<? extends GrantedAuthority> authorities) {
        this.id = id;
        this.username = username;
        this.password = password;
        this.state = state;
        this.email = email;
        this.lastPasswordResetDate = lastPasswordResetDate;
        this.enabled = enabled;
        this.loginTime = loginTime;
        this.authorities = authorities;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }

    @JsonIgnore   //将JwtUser序列化时,有些属性的值我们是不序列化出来的,所以可以加这个注解
    @Override
    public String getPassword() {
        return this.password;
    }

    @Override
    public String getUsername() {
        return this.username;
    }

    @JsonIgnore
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @JsonIgnore
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @JsonIgnore
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @JsonIgnore
    @Override
    public boolean isEnabled() {
        return this.enabled;
    }

    @JsonIgnore
    public Integer getId() {
        return id;
    }

    public Integer getState() {
        return state;
    }

    public String getEmail() {
        return email;
    }

    @JsonIgnore
    public Date getLastPasswordResetDate() {
        return lastPasswordResetDate;
    }

    public Date getLoginTime() {
        return loginTime;
    }
    
}
这个类里面有几个注意点:

1)里面所有的属性应该是final的

2)里面的角色集合不是用户的角色集合,而是授权的角色集合,是应该继承 GrantedAuthority的(也就是做个转化,把用户自己定义的角色去实现GrantedAuthority类型,再重新放在集合里面,赋给JwtUser)

3)JwtUser里面的id,name,password,enabled,List<Authortiy>这几个属性名是不能改的,也是必须要有的(继承UserDetail的原因),其他属性就看你自己的需求了


好了,JwtUser类创建好了,但是我们该怎么得到呢,所以我们建一个JwtUserFactory来生成JwtUser( 工厂模式)

package com.yy.hospital.security.domain;

import com.yy.hospital.domain.Admins;
import com.yy.hospital.domain.Authority;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;

import java.util.List;
import java.util.stream.Collectors;

public final class JwtUserFactory {

    private JwtUserFactory(){
    }

    //创建JwtUser的方法
    public static JwtUser create(Admins user){
        return new JwtUser(user.getAid(),
                user.getAname(),
                user.getPwd(),
                user.getState(),
                user.getEmail(),
                user.getLastPasswordResetDate(),
                user.getAexist()==1?true:false,
                user.getLoginTime(),
                mapToGrantedAuthorities(user.getAuthorities())); //调用下面的静态方法
    }

    /*
        将查询的用户角色集合转换成security框架授权的角色集合
     */
    private static List<GrantedAuthority> mapToGrantedAuthorities(List<Authority> authorities){
        return authorities.stream()
                .map(authority -> new SimpleGrantedAuthority(authority.getName().name()))
                .collect(Collectors.toList());
        //把集合变成流,然后用函数式编程的方法处理,最后把流又变成集合

    }
}

                                                                                                           

其中JwtUserFactory类中create方法需要的参数Admin是怎么来呢,所以我们还要建立一个JwtUserDetailsService,来生成Admin。这个服务类必须实现UserDetailsService接口,然后会实现一个loadUserByUsername(String username)的方法,他会返回一个JwtUser

package com.yy.hospital.security.service;

import com.yy.hospital.domain.Admins;
import com.yy.hospital.mapper.AdminsMapper;
import com.yy.hospital.security.domain.JwtUserFactory;
import org.springframework.beans.factory.annotation.Autowired;
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.Service;

@Service
public class JwtUserDetailsService implements UserDetailsService {

    @Autowired
    private AdminsMapper adminsMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Admins user = adminsMapper.findByName(username);
        if(user==null){
            throw new UsernameNotFoundException("用户名不存在!");
        }else{
            return JwtUserFactory.create(user);
        }
    }
}

里面的adminsMapper.findByName(username)的实现过程如下:





这样,最后就返回一个实现了UserDetails接口的JwtUser对象。

最后,我们再写一个安全配置类:WebSecurityConfig,

package com.yy.hospital.security.config;
import com.yy.hospital.security.service.JwtUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
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.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;



/**
 * 安全配置类
 * 配置哪些请求要经过安全检查
 */
@SuppressWarnings("SpringJavaAutowiringInspection")   //抑制了一个警告
@Configuration
@EnableWebSecurity    //启用web安全检查
@EnableGlobalMethodSecurity(prePostEnabled = true)    //启用全局方法的安全检查(预处理预授权的属性为true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{



    @Autowired
    //限定实现类实例名
    @Qualifier("jwtUserDetailsService")   //限定接口UserDetailsService必须绑jwtUserDetailsService
    private UserDetailsService UserDetailsService;


    //全局配置
    @Autowired
    public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        authenticationManagerBuilder
                .userDetailsService(this.UserDetailsService)
                .passwordEncoder(passwordEncoder());
    }


    //强hash加密
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }


    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        //安全配置
        httpSecurity
                // we don't need CSRF because our token is invulnerable
                .csrf().disable()

                // don't create session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()

                .authorizeRequests()
                //.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()

                // allow anonymous resource requests
                .antMatchers(
                        HttpMethod.GET,
                        "/",
                        "/*.html",
                        "/favicon.ico",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js"
                ).permitAll()

                // Un-secure 注册 登录 验证码
                //放在这里面是任何人都可以访问的--不做安全检查的
                .antMatchers(
                        "/auth/**",
                        "/api/users",
                        "/api/imagecode",//  验证码
                        "/api/testError"
                ).permitAll()
                // secure other api
                // 其他api请求都必须做安全校验
                .anyRequest().authenticated();  //除了上面申明的其余的都要权限访问

        // disable page caching
        httpSecurity
                .headers()
                .frameOptions().sameOrigin()  // required to set for H2 else H2 Console will be blank.
                .cacheControl();
    }
}
 
 

好,我们现在可以阶段性的测试下!



当访问WebSecurityConfig里面允许访问的api/users时,是能请求到,有返回值的,而请求没有申明的api/usersession时,是403错误,所以阶段性成功啦!


猜你喜欢

转载自blog.csdn.net/qq_36582604/article/details/80976144