通常情况下,把API直接暴露出去是风险很大的,不说别的,直接被机器攻击就够喝一壶的。那么一般来说,对API要划分出一定的权限级别,然后做一个用户的鉴权,依据鉴权结果给予用户开放对应的API。目前,比较主流的方案有几种:
1.用户名和密码鉴权,使用Session保存用户鉴权结果。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.用户导航到登录页,输入用户名、密码,进行登录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 authorities;
//这里,getter和setter省略
}
注意,Admin类里面有一个角色集合List
好,现在要添加核心类了,首先建立个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
好了,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 mapToGrantedAuthorities(List 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错误,所以阶段性成功啦!