关键词

Spring Boot`、`OAuth 2.0`、`JWT`、`Spring Security`、`SSO`、`UAA

写在前面

最近安静下来,重新学习一些东西,最近一年几乎没写过代码。整天疲于奔命的日子终于结束了。坐下来,弄杯咖啡,思考一些问题,挺好。这几天有人问我Spring Boot结合Spring Security实现OAuth认证的问题,写了个Demo,顺便分享下。Spring 2之后就没再用过Java,主要是xml太麻烦,就投入了Node.js的怀抱,现在Java倒是好过之前很多,无论是执行效率还是其他什么。感谢Pivotal团队在Spring boot上的努力,感谢Josh Long,一个有意思的攻城狮。

我又搞Java也是为了去折腾微服务,因为目前看国内就Java程序猿最好找,虽然水平好的难找,但是至少能找到,不像其他编程语言,找个会世界上最好的编程语言PHP的人真的不易。

Spring Boot

有了Spring Boot这样的神器,可以很简单的使用强大的Spring框架。你需要关心的事儿只是创建应用,不必再配置了,“Just run!”,这可是Josh Long每次演讲必说的,他的另一句必须说的就是“make jar not war”,这意味着,不用太关心是Tomcat还是Jetty或者Undertow了。专心解决逻辑问题,这当然是个好事儿,部署简单了很多。

创建Spring Boot应用

有很多方法去创建Spring Boot项目,官方也推荐用:

  • Spring Boot在线项目创建
  • CLI 工具

start.spring.io可以方便选择你要用的组件,命令行工具当然也可以。目前Spring Boot已经到了1.53,我是懒得去更新依赖,继续用1.52版本。虽然阿里也有了中央库的国内版本不知道是否稳定。如果你感兴趣,可以自己尝试下。你可以选Maven或者Gradle成为你项目的构建工具,Gradle优雅一些,使用了Groovy语言进行描述。

打开start.spring.io,创建的项目只需要一个Dependency,也就是Web,然后下载项目,用IntellJ IDEA打开。我的Java版本是1.8。

这里看下整个项目的pom.xml文件中的依赖部分:

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency> <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>复制代码

  

 

所有Spring Boot相关的依赖都是以starter形式出现,这样你无需关心版本和相关的依赖,所以这样大大简化了开发过程。

当你在pom文件中集成了spring-boot-maven-plugin插件后你可以使用Maven相关的命令来run你的应用。例如mvn spring-boot:run,这样会启动一个嵌入式的Tomcat,并运行在8080端口,直接访问你当然会获得一个Whitelabel Error Page,这说明Tomcat已经启动了。

创建一个Web 应用

这还是一篇关于Web安全的文章,但是也得先有个简单的HTTP请求响应。我们先弄一个可以返回JSON的Controller。修改程序的入口文件:

@SpringBootApplication
@RestController
@EnableAutoConfiguration
public class DemoApplication { // main函数,Spring Boot程序入口
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
} // 根目录映射 Get访问方式 直接返回一个字符串
@RequestMapping("/")
Map<String, String> hello() {
// 返回map会变成JSON key value方式
Map<String,String> map=new HashMap<String,String>();
map.put("content", "hello freewolf~");
return map;
}
}

  这里我尽量的写清楚,让不了解Spring Security的人通过这个例子可以了解这个东西,很多人都觉得它很复杂,而投向了Apache Shiro,其实这个并不难懂。知道主要的处理流程,和这个流程中哪些类都起了哪些作用就好了。

Spring Boot对于开发人员最大的好处在于可以对Spring应用进行自动配置。Spring Boot会根据应用中声明的第三方依赖来自动配置Spring框架,而不需要进行显式的声明。Spring Boot推荐采用基于Java注解的配置方式,而不是传统的XML。只需要在主配置 Java 类上添加@EnableAutoConfiguration注解就可以启用自动配置。Spring Boot的自动配置功能是没有侵入性的,只是作为一种基本的默认实现。

这个入口类我们添加@RestController@EnableAutoConfiguration两个注解。@RestController注解相当于@ResponseBody@Controller合在一起的作用。

run整个项目。访问http://localhost:8080/就能看到这个JSON的输出。使用Chrome浏览器可以装JSON Formatter这个插件,显示更PL一些。

{
"content": "hello freewolf~"
}

  为了显示统一的JSON返回,这里建立一个JSONResult类进行,简单的处理。首先修改pom.xml,加入org.json相关依赖。

<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
</dependency>

  然后在我们的代码中加入一个新的类,里面只有一个结果集处理方法,因为只是个Demo,所有这里都放在一个文件中。这个类只是让返回的JSON结果变为三部分:

  • status - 返回状态码 0 代表正常返回,其他都是错误
  • message - 一般显示错误信息
  • result - 结果集
class JSONResult{
public static String fillResultString(Integer status, String message, Object result){
JSONObject jsonObject = new JSONObject(){{
put("status", status);
put("message", message);
put("result", result);
}};
return jsonObject.toString();
}
}

  然后我们引入一个新的@RestController并返回一些简单的结果,后面我们将对这些内容进行访问控制,这里用到了上面的结果集处理类。这里多放两个方法,后面我们来测试权限和角色的验证用。

@RestController
class UserController { // 路由映射到/users
@RequestMapping(value = "/users", produces="application/json;charset=UTF-8")
public String usersList() { ArrayList<String> users = new ArrayList<String>(){{
add("freewolf");
add("tom");
add("jerry");
}}; return JSONResult.fillResultString(0, "", users);
} @RequestMapping(value = "/hello", produces="application/json;charset=UTF-8")
public String hello() {
ArrayList<String> users = new ArrayList<String>(){{ add("hello"); }};
return JSONResult.fillResultString(0, "", users);
} @RequestMapping(value = "/world", produces="application/json;charset=UTF-8")
public String world() {
ArrayList<String> users = new ArrayList<String>(){{ add("world"); }};
return JSONResult.fillResultString(0, "", users);
}
}

  重新run这个文件,访问http://localhost:8080/users就看到了下面的结果:

{
"result": [
"freewolf",
"tom",
"jerry"
],
"message": "",
"status": 0
}

  如果你细心,你会发现这里的JSON返回时,Chrome的格式化插件好像并没有识别?这是为什么呢?我们借助curl分别看一下我们写的两个方法的Header信息.

curl -I http://127.0.0.1:8080/
curl -I http://127.0.0.1:8080/users复制代码

  可以看到第一个方法hello,由于返回值是Map,Spring已经有相关的机制自动处理成JSON:

可以看到第一个方法hello,由于返回值是Map,Spring已经有相关的机制自动处理成JSON:

  第二个方法usersList由于返回时String,由于是@RestControler已经含有了@ResponseBody也就是直接返回内容,并不模板。所以就是:

Content-Type: text/plain;charset=UTF-8复制代码

  那怎么才能让它变成JSON呢,其实也很简单只需要补充一下相关注解:

@RequestMapping(value = "/users", produces="application/json;charset=UTF-8")复制代码

  这样就好了。

使用JWT保护你的Spring Boot应用

终于我们开始介绍正题,这里我们会对/users进行访问控制,先通过申请一个JWT(JSON Web Token读jot),然后通过这个访问/users,才能拿到数据。

关于JWT,出门奔向以下内容,这些不在本文讨论范围内:

  • RFC7519
  • JWT

JWT很大程度上还是个新技术,通过使用HMAC(Hash-based Message Authentication Code)计算信息摘要,也可以用RSA公私钥中的私钥进行签名。这个根据业务场景进行选择。

添加Spring Security

根据上文我们说过我们要对/users进行访问控制,让用户在/login进行登录并获得Token。这里我们需要将spring-boot-starter-security加入pom.xml。加入后,我们的Spring Boot项目将需要提供身份验证,相关的pom.xml如下:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>

  至此我们之前所有的路由都需要身份验证。我们将引入一个安全设置类WebSecurityConfig,这个类需要从WebSecurityConfigurerAdapter类继承。

@Configuration
@EnableWebSecurity
class WebSecurityConfig extends WebSecurityConfigurerAdapter { // 设置 HTTP 验证规则
@Override
protected void configure(HttpSecurity http) throws Exception {
// 关闭csrf验证
http.csrf().disable()
// 对请求进行认证
.authorizeRequests()
// 所有 / 的所有请求 都放行
.antMatchers("/").permitAll()
// 所有 /login 的POST请求 都放行
.antMatchers(HttpMethod.POST, "/login").permitAll()
// 权限检查
.antMatchers("/hello").hasAuthority("AUTH_WRITE")
// 角色检查
.antMatchers("/world").hasRole("ADMIN")
// 所有请求需要身份认证
.anyRequest().authenticated()
.and()
// 添加一个过滤器 所有访问 /login 的请求交给 JWTLoginFilter 来处理 这个类处理所有的JWT相关内容
.addFilterBefore(new JWTLoginFilter("/login", authenticationManager()),
UsernamePasswordAuthenticationFilter.class)
// 添加一个过滤器验证其他请求的Token是否合法
.addFilterBefore(new JWTAuthenticationFilter(),
UsernamePasswordAuthenticationFilter.class);
} @Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 使用自定义身份验证组件
auth.authenticationProvider(new CustomAuthenticationProvider()); }
}

  先放两个基本类,一个负责存储用户名密码,另一个是一个权限类型,负责存储权限和角色

class AccountCredentials {

    private String username;
private String password; public String getUsername() {
return username;
} public void setUsername(String username) {
this.username = username;
} public String getPassword() {
return password;
} public void setPassword(String password) {
this.password = password;
}
} class GrantedAuthorityImpl implements GrantedAuthority{
private String authority; public GrantedAuthorityImpl(String authority) {
this.authority = authority;
} public void setAuthority(String authority) {
this.authority = authority;
} @Override
public String getAuthority() {
return this.authority;
}
}

在上面的安全设置类中,我们设置所有人都能访问/POST方式访问/login,其他的任何路由都需要进行认证。然后将所有访问/login的请求,都交给JWTLoginFilter过滤器来处理。稍后我们会创建这个过滤器和其他这里需要的JWTAuthenticationFilterCustomAuthenticationProvider两个类。

先建立一个JWT生成,和验签的类

class TokenAuthenticationService {
static final long EXPIRATIONTIME = 432_000_000; // 5天
static final String SECRET = "P@ssw02d"; // JWT密码
static final String TOKEN_PREFIX = "Bearer"; // Token前缀
static final String HEADER_STRING = "Authorization";// 存放Token的Header Key // JWT生成方法
static void addAuthentication(HttpServletResponse response, String username) { // 生成JWT
String JWT = Jwts.builder()
// 保存权限(角色)
.claim("authorities", "ROLE_ADMIN,AUTH_WRITE")
// 用户名写入标题
.setSubject(username)
// 有效期设置
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATIONTIME))
// 签名设置
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact(); // 将 JWT 写入 body
try {
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_OK);
response.getOutputStream().println(JSONResult.fillResultString(0, "", JWT));
} catch (IOException e) {
e.printStackTrace();
}
} // JWT验证方法
static Authentication getAuthentication(HttpServletRequest request) {
// 从Header中拿到token
String token = request.getHeader(HEADER_STRING); if (token != null) {
// 解析 Token
Claims claims = Jwts.parser()
// 验签
.setSigningKey(SECRET)
// 去掉 Bearer
.parseClaimsJws(token.replace(TOKEN_PREFIX, ""))
.getBody(); // 拿用户名
String user = claims.getSubject(); // 得到 权限(角色)
List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList((String) claims.get("authorities")); // 返回验证令牌
return user != null ?
new UsernamePasswordAuthenticationToken(user, null, authorities) :
null;
}
return null;
}
}

  这个类就两个static方法,一个负责生成JWT,一个负责认证JWT最后生成验证令牌。注释已经写得很清楚了,这里不多说了。

下面来看自定义验证组件,这里简单写了,这个类就是提供密码验证功能,在实际使用时换成自己相应的验证逻辑,从数据库中取出、比对、赋予用户相应权限。

// 自定义身份认证验证组件
class CustomAuthenticationProvider implements AuthenticationProvider { @Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 获取认证的用户名 & 密码
String name = authentication.getName();
String password = authentication.getCredentials().toString(); // 认证逻辑
if (name.equals("admin") && password.equals("123456")) { // 这里设置权限和角色
ArrayList<GrantedAuthority> authorities = new ArrayList<>();
authorities.add( new GrantedAuthorityImpl("ROLE_ADMIN") );
authorities.add( new GrantedAuthorityImpl("AUTH_WRITE") );
// 生成令牌
Authentication auth = new UsernamePasswordAuthenticationToken(name, password, authorities);
return auth;
}else {
throw new BadCredentialsException("密码错误~");
}
} // 是否可以提供输入类型的认证服务
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}

  下面实现JWTLoginFilter 这个Filter比较简单,除了构造函数需要重写三个方法。

  • attemptAuthentication - 登录时需要验证时候调用
  • successfulAuthentication - 验证成功后调用
  • unsuccessfulAuthentication - 验证失败后调用,这里直接灌入500错误返回,由于同一JSON返回,HTTP就都返回200了
class JWTLoginFilter extends AbstractAuthenticationProcessingFilter {

    public JWTLoginFilter(String url, AuthenticationManager authManager) {
super(new AntPathRequestMatcher(url));
setAuthenticationManager(authManager);
} @Override
public Authentication attemptAuthentication(
HttpServletRequest req, HttpServletResponse res)
throws AuthenticationException, IOException, ServletException { // JSON反序列化成 AccountCredentials
AccountCredentials creds = new ObjectMapper().readValue(req.getInputStream(), AccountCredentials.class); // 返回一个验证令牌
return getAuthenticationManager().authenticate(
new UsernamePasswordAuthenticationToken(
creds.getUsername(),
creds.getPassword()
)
);
} @Override
protected void successfulAuthentication(
HttpServletRequest req,
HttpServletResponse res, FilterChain chain,
Authentication auth) throws IOException, ServletException { TokenAuthenticationService.addAuthentication(res, auth.getName());
} @Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_OK);
response.getOutputStream().println(JSONResult.fillResultString(500, "Internal Server Error!!!", JSONObject.NULL));
}
}

  再完成最后一个类JWTAuthenticationFilter,这也是个拦截器,它拦截所有需要JWT的请求,然后调用TokenAuthenticationService类的静态方法去做JWT验证。

class JWTAuthenticationFilter extends GenericFilterBean {

    @Override
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain filterChain)
throws IOException, ServletException {
Authentication authentication = TokenAuthenticationService
.getAuthentication((HttpServletRequest)request); SecurityContextHolder.getContext()
.setAuthentication(authentication);
filterChain.doFilter(request,response);
}
}

  现在代码就写完了,整个Spring Security结合JWT基本就差不多了,下面我们来测试下,并说下整体流程。

开始测试,先运行整个项目,这里介绍下过程:

  • 先程序启动 - main函数
  • 注册验证组件 - WebSecurityConfig 类 configure(AuthenticationManagerBuilder auth)方法,这里我们注册了自定义验证组件
  • 设置验证规则 - WebSecurityConfig 类 configure(HttpSecurity http)方法,这里设置了各种路由访问规则
  • 初始化过滤组件 - JWTLoginFilter 和 JWTAuthenticationFilter 类会初始化

首先测试获取Token,这里使用CURL命令行工具来测试。

curl -H "Content-Type: application/json" -X POST -d '{"username":"admin","password":"123456"}'  http://127.0.0.1:8080/login复制代码

  

结果:

{
"result": "eyJhbGciOiJIUzUxMiJ9.eyJhdXRob3JpdGllcyI6IlJPTEVfQURNSU4sQVVUSF9XUklURSIsInN1YiI6ImFkbWluIiwiZXhwIjoxNDkzNzgyMjQwfQ.HNfV1CU2CdAnBTH682C5-KOfr2P71xr9PYLaLpDVhOw8KWWSJ0lBo0BCq4LoNwsK_Y3-W3avgbJb0jW9FNYDRQ",
"message": "",
"status": 0
}

  这里我们得到了相关的JWT,反Base64之后,就是下面的内容,标准JWT

{"alg":"HS512"}{"authorities":"ROLE_ADMIN,AUTH_WRITE","sub":"admin","exp":1493782240}ͽ]BS`pS6 ~ hCVH%
ܬ)֝ଖoE5р复制代码

 

 整个过程如下:

拿到传入JSON,解析用户名密码 - JWTLoginFilter 类 attemptAuthentication 方法
自定义身份认证验证组件,进行身份认证 - CustomAuthenticationProvider 类 authenticate 方法
盐城成功 - JWTLoginFilter 类 successfulAuthentication 方法
生成JWT - TokenAuthenticationService 类 addAuthentication方法 

再测试一个访问资源的: 

curl -H "Content-Type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJhdXRob3JpdGllcyI6IlJPTEVfQURNSU4sQVVUSF9XUklURSIsInN1YiI6ImFkbWluIiwiZXhwIjoxNDkzNzgyMjQwfQ.HNfV1CU2CdAnBTH682C5-KOfr2P71xr9PYLaLpDVhOw8KWWSJ0lBo0BCq4LoNwsK_Y3-W3avgbJb0jW9FNYDRQ"  http://127.0.0.1:8080/users复制代码

  

{
"result":["freewolf","tom","jerry"],
"message":"",
"status":0
}

  说明我们的Token生效可以正常访问。其他的结果您可以自己去测试。再回到处理流程:

  • 接到请求进行拦截 - JWTAuthenticationFilter 中的方法
  • 验证JWT - TokenAuthenticationService 类 getAuthentication 方法
  • 访问Controller

这样本文的主要流程就结束了,本文主要介绍了,如何用Spring Security结合JWT保护你的Spring Boot应用。如何使用RoleAuthority,这里多说一句其实在Spring Security中,对于GrantedAuthority接口实现类来说是不区分是Role还是Authority,二者区别就是如果是hasAuthority判断,就是判断整个字符串,判断hasRole时,系统自动加上ROLE_到判断的Role字符串上,也就是说hasRole("CREATE")hasAuthority('ROLE_CREATE')是相同的。利用这些可以搭建完整的RBAC体系。本文到此,你已经会用了本文介绍的知识点。

代码地址

https://github.com/freew01f/securing-spring-boot-with-jwts

创建一个Web 应用

这还是一篇关于Web安全的文章,但是也得先有个简单的HTTP请求响应。我们先弄一个可以返回JSON的Controller。修改程序的入口文件:

@SpringBootApplication
@RestController
@EnableAutoConfiguration
public class DemoApplication {

    // main函数,Spring Boot程序入口
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    // 根目录映射 Get访问方式 直接返回一个字符串
    @RequestMapping("/")
    Map<String, String> hello() {
      // 返回map会变成JSON key value方式
      Map<String,String> map=new HashMap<String,String>();
      map.put("content", "hello freewolf~");
      return map;
    }
}复制代码

这里我尽量的写清楚,让不了解Spring Security的人通过这个例子可以了解这个东西,很多人都觉得它很复杂,而投向了Apache Shiro,其实这个并不难懂。知道主要的处理流程,和这个流程中哪些类都起了哪些作用就好了。

Spring Boot对于开发人员最大的好处在于可以对Spring应用进行自动配置。Spring Boot会根据应用中声明的第三方依赖来自动配置Spring框架,而不需要进行显式的声明。Spring Boot推荐采用基于Java注解的配置方式,而不是传统的XML。只需要在主配置 Java 类上添加@EnableAutoConfiguration注解就可以启用自动配置。Spring Boot的自动配置功能是没有侵入性的,只是作为一种基本的默认实现。

这个入口类我们添加@RestController@EnableAutoConfiguration两个注解。@RestController注解相当于@ResponseBody@Controller合在一起的作用。

run整个项目。访问http://localhost:8080/就能看到这个JSON的输出。使用Chrome浏览器可以装JSON Formatter这个插件,显示更PL一些。

{
  "content": "hello freewolf~"
}复制代码

为了显示统一的JSON返回,这里建立一个JSONResult类进行,简单的处理。首先修改pom.xml,加入org.json相关依赖。

<dependency>
    <groupId>org.json</groupId>
    <artifactId>json</artifactId>
</dependency>复制代码

然后在我们的代码中加入一个新的类,里面只有一个结果集处理方法,因为只是个Demo,所有这里都放在一个文件中。这个类只是让返回的JSON结果变为三部分:

  • status - 返回状态码 0 代表正常返回,其他都是错误
  • message - 一般显示错误信息
  • result - 结果集
class JSONResult{
    public static String fillResultString(Integer status, String message, Object result){
        JSONObject jsonObject = new JSONObject(){{
            put("status", status);
            put("message", message);
            put("result", result);
        }};
        return jsonObject.toString();
    }
}复制代码

然后我们引入一个新的@RestController并返回一些简单的结果,后面我们将对这些内容进行访问控制,这里用到了上面的结果集处理类。这里多放两个方法,后面我们来测试权限和角色的验证用。

@RestController
class UserController {

    // 路由映射到/users
    @RequestMapping(value = "/users", produces="application/json;charset=UTF-8")
    public String usersList() {

        ArrayList<String> users =  new ArrayList<String>(){{
            add("freewolf");
            add("tom");
            add("jerry");
        }};

        return JSONResult.fillResultString(0, "", users);
    }

    @RequestMapping(value = "/hello", produces="application/json;charset=UTF-8")
    public String hello() {
        ArrayList<String> users =  new ArrayList<String>(){{ add("hello"); }};
        return JSONResult.fillResultString(0, "", users);
    }

    @RequestMapping(value = "/world", produces="application/json;charset=UTF-8")
    public String world() {
        ArrayList<String> users =  new ArrayList<String>(){{ add("world"); }};
        return JSONResult.fillResultString(0, "", users);
    }
}复制代码

重新run这个文件,访问http://localhost:8080/users就看到了下面的结果:

{
  "result": [
    "freewolf",
    "tom",
    "jerry"
  ],
  "message": "",
  "status": 0
}复制代码

如果你细心,你会发现这里的JSON返回时,Chrome的格式化插件好像并没有识别?这是为什么呢?我们借助curl分别看一下我们写的两个方法的Header信息.

curl -I http://127.0.0.1:8080/
curl -I http://127.0.0.1:8080/users复制代码

可以看到第一个方法hello,由于返回值是Map,Spring已经有相关的机制自动处理成JSON:

Content-Type: application/json;charset=UTF-8复制代码

第二个方法usersList由于返回时String,由于是@RestControler已经含有了@ResponseBody也就是直接返回内容,并不模板。所以就是:

Content-Type: text/plain;charset=UTF-8复制代码

那怎么才能让它变成JSON呢,其实也很简单只需要补充一下相关注解:

@RequestMapping(value = "/users", produces="application/json;charset=UTF-8")复制代码

这样就好了。

使用JWT保护你的Spring Boot应用

终于我们开始介绍正题,这里我们会对/users进行访问控制,先通过申请一个JWT(JSON Web Token读jot),然后通过这个访问/users,才能拿到数据。

关于JWT,出门奔向以下内容,这些不在本文讨论范围内:

  • RFC7519
  • JWT

JWT很大程度上还是个新技术,通过使用HMAC(Hash-based Message Authentication Code)计算信息摘要,也可以用RSA公私钥中的私钥进行签名。这个根据业务场景进行选择。

添加Spring Security

根据上文我们说过我们要对/users进行访问控制,让用户在/login进行登录并获得Token。这里我们需要将spring-boot-starter-security加入pom.xml。加入后,我们的Spring Boot项目将需要提供身份验证,相关的pom.xml如下:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.7.0</version>
</dependency>复制代码

至此我们之前所有的路由都需要身份验证。我们将引入一个安全设置类WebSecurityConfig,这个类需要从WebSecurityConfigurerAdapter类继承。

@Configuration
@EnableWebSecurity
class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    // 设置 HTTP 验证规则
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 关闭csrf验证
        http.csrf().disable()
                // 对请求进行认证
                .authorizeRequests()
                // 所有 / 的所有请求 都放行
                .antMatchers("/").permitAll()
                // 所有 /login 的POST请求 都放行
                .antMatchers(HttpMethod.POST, "/login").permitAll()
                // 权限检查
                .antMatchers("/hello").hasAuthority("AUTH_WRITE")
                // 角色检查
                .antMatchers("/world").hasRole("ADMIN")
                // 所有请求需要身份认证
                .anyRequest().authenticated()
            .and()
                // 添加一个过滤器 所有访问 /login 的请求交给 JWTLoginFilter 来处理 这个类处理所有的JWT相关内容
                .addFilterBefore(new JWTLoginFilter("/login", authenticationManager()),
                        UsernamePasswordAuthenticationFilter.class)
                // 添加一个过滤器验证其他请求的Token是否合法
                .addFilterBefore(new JWTAuthenticationFilter(),
                        UsernamePasswordAuthenticationFilter.class);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 使用自定义身份验证组件
        auth.authenticationProvider(new CustomAuthenticationProvider());

    }
}复制代码

先放两个基本类,一个负责存储用户名密码,另一个是一个权限类型,负责存储权限和角色。

class AccountCredentials {

    private String username;
    private String password;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

class GrantedAuthorityImpl implements GrantedAuthority{
    private String authority;

    public GrantedAuthorityImpl(String authority) {
        this.authority = authority;
    }

    public void setAuthority(String authority) {
        this.authority = authority;
    }

    @Override
    public String getAuthority() {
        return this.authority;
    }
}复制代码

在上面的安全设置类中,我们设置所有人都能访问/POST方式访问/login,其他的任何路由都需要进行认证。然后将所有访问/login的请求,都交给JWTLoginFilter过滤器来处理。稍后我们会创建这个过滤器和其他这里需要的JWTAuthenticationFilterCustomAuthenticationProvider两个类。

先建立一个JWT生成,和验签的类

class TokenAuthenticationService {
    static final long EXPIRATIONTIME = 432_000_000;     // 5天
    static final String SECRET = "P@ssw02d";            // JWT密码
    static final String TOKEN_PREFIX = "Bearer";        // Token前缀
    static final String HEADER_STRING = "Authorization";// 存放Token的Header Key

  // JWT生成方法
    static void addAuthentication(HttpServletResponse response, String username) {

    // 生成JWT
        String JWT = Jwts.builder()
                // 保存权限(角色)
                .claim("authorities", "ROLE_ADMIN,AUTH_WRITE")
                // 用户名写入标题
                .setSubject(username)
                // 有效期设置
                        .setExpiration(new Date(System.currentTimeMillis() + EXPIRATIONTIME))
                // 签名设置
                        .signWith(SignatureAlgorithm.HS512, SECRET)
                        .compact();

        // 将 JWT 写入 body
        try {
            response.setContentType("application/json");
            response.setStatus(HttpServletResponse.SC_OK);
            response.getOutputStream().println(JSONResult.fillResultString(0, "", JWT));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

  // JWT验证方法
    static Authentication getAuthentication(HttpServletRequest request) {
        // 从Header中拿到token
        String token = request.getHeader(HEADER_STRING);

        if (token != null) {
            // 解析 Token
            Claims claims = Jwts.parser()
                    // 验签
                    .setSigningKey(SECRET)
                    // 去掉 Bearer
                    .parseClaimsJws(token.replace(TOKEN_PREFIX, ""))
                    .getBody();

            // 拿用户名
            String user = claims.getSubject();

            // 得到 权限(角色)
            List<GrantedAuthority> authorities =  AuthorityUtils.commaSeparatedStringToAuthorityList((String) claims.get("authorities"));

            // 返回验证令牌
            return user != null ?
                    new UsernamePasswordAuthenticationToken(user, null, authorities) :
                    null;
        }
        return null;
    }
}复制代码

这个类就两个static方法,一个负责生成JWT,一个负责认证JWT最后生成验证令牌。注释已经写得很清楚了,这里不多说了。

下面来看自定义验证组件,这里简单写了,这个类就是提供密码验证功能,在实际使用时换成自己相应的验证逻辑,从数据库中取出、比对、赋予用户相应权限。

// 自定义身份认证验证组件
class CustomAuthenticationProvider implements AuthenticationProvider {

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // 获取认证的用户名 & 密码
        String name = authentication.getName();
        String password = authentication.getCredentials().toString();

        // 认证逻辑
        if (name.equals("admin") && password.equals("123456")) {

            // 这里设置权限和角色
            ArrayList<GrantedAuthority> authorities = new ArrayList<>();
            authorities.add( new GrantedAuthorityImpl("ROLE_ADMIN") );
            authorities.add( new GrantedAuthorityImpl("AUTH_WRITE") );
            // 生成令牌
            Authentication auth = new UsernamePasswordAuthenticationToken(name, password, authorities);
            return auth;
        }else {
            throw new BadCredentialsException("密码错误~");
        }
    }

    // 是否可以提供输入类型的认证服务
    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}复制代码

下面实现JWTLoginFilter 这个Filter比较简单,除了构造函数需要重写三个方法。

  • attemptAuthentication - 登录时需要验证时候调用
  • successfulAuthentication - 验证成功后调用
  • unsuccessfulAuthentication - 验证失败后调用,这里直接灌入500错误返回,由于同一JSON返回,HTTP就都返回200了
class JWTLoginFilter extends AbstractAuthenticationProcessingFilter {

    public JWTLoginFilter(String url, AuthenticationManager authManager) {
        super(new AntPathRequestMatcher(url));
        setAuthenticationManager(authManager);
    }

    @Override
    public Authentication attemptAuthentication(
            HttpServletRequest req, HttpServletResponse res)
            throws AuthenticationException, IOException, ServletException {

        // JSON反序列化成 AccountCredentials
        AccountCredentials creds = new ObjectMapper().readValue(req.getInputStream(), AccountCredentials.class);

        // 返回一个验证令牌
        return getAuthenticationManager().authenticate(
                new UsernamePasswordAuthenticationToken(
                        creds.getUsername(),
                        creds.getPassword()
                )
        );
    }

    @Override
    protected void successfulAuthentication(
            HttpServletRequest req,
            HttpServletResponse res, FilterChain chain,
            Authentication auth) throws IOException, ServletException {

        TokenAuthenticationService.addAuthentication(res, auth.getName());
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {

        response.setContentType("application/json");
        response.setStatus(HttpServletResponse.SC_OK);
        response.getOutputStream().println(JSONResult.fillResultString(500, "Internal Server Error!!!", JSONObject.NULL));
    }
}复制代码

再完成最后一个类JWTAuthenticationFilter,这也是个拦截器,它拦截所有需要JWT的请求,然后调用TokenAuthenticationService类的静态方法去做JWT验证。

class JWTAuthenticationFilter extends GenericFilterBean {

    @Override
    public void doFilter(ServletRequest request,
                         ServletResponse response,
                         FilterChain filterChain)
            throws IOException, ServletException {
        Authentication authentication = TokenAuthenticationService
                .getAuthentication((HttpServletRequest)request);

        SecurityContextHolder.getContext()
                .setAuthentication(authentication);
        filterChain.doFilter(request,response);
    }
}复制代码

现在代码就写完了,整个Spring Security结合JWT基本就差不多了,下面我们来测试下,并说下整体流程。

开始测试,先运行整个项目,这里介绍下过程:

  • 先程序启动 - main函数
  • 注册验证组件 - WebSecurityConfig 类 configure(AuthenticationManagerBuilder auth)方法,这里我们注册了自定义验证组件
  • 设置验证规则 - WebSecurityConfig 类 configure(HttpSecurity http)方法,这里设置了各种路由访问规则
  • 初始化过滤组件 - JWTLoginFilter 和 JWTAuthenticationFilter 类会初始化

首先测试获取Token,这里使用CURL命令行工具来测试。

curl -H "Content-Type: application/json" -X POST -d '{"username":"admin","password":"123456"}'  http://127.0.0.1:8080/login复制代码

结果:

{
  "result": "eyJhbGciOiJIUzUxMiJ9.eyJhdXRob3JpdGllcyI6IlJPTEVfQURNSU4sQVVUSF9XUklURSIsInN1YiI6ImFkbWluIiwiZXhwIjoxNDkzNzgyMjQwfQ.HNfV1CU2CdAnBTH682C5-KOfr2P71xr9PYLaLpDVhOw8KWWSJ0lBo0BCq4LoNwsK_Y3-W3avgbJb0jW9FNYDRQ",
  "message": "",
  "status": 0
}复制代码

这里我们得到了相关的JWT,反Base64之后,就是下面的内容,标准JWT

{"alg":"HS512"}{"authorities":"ROLE_ADMIN,AUTH_WRITE","sub":"admin","exp":1493782240}ͽ]BS`pS6 ~ hCVH%
 ܬ)֝ଖoE5р复制代码

整个过程如下:

  • 拿到传入JSON,解析用户名密码 - JWTLoginFilter 类 attemptAuthentication 方法
  • 自定义身份认证验证组件,进行身份认证 - CustomAuthenticationProvider 类 authenticate 方法
  • 盐城成功 -  JWTLoginFilter 类 successfulAuthentication 方法
  • 生成JWT - TokenAuthenticationService 类 addAuthentication方法

再测试一个访问资源的:

curl -H "Content-Type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJhdXRob3JpdGllcyI6IlJPTEVfQURNSU4sQVVUSF9XUklURSIsInN1YiI6ImFkbWluIiwiZXhwIjoxNDkzNzgyMjQwfQ.HNfV1CU2CdAnBTH682C5-KOfr2P71xr9PYLaLpDVhOw8KWWSJ0lBo0BCq4LoNwsK_Y3-W3avgbJb0jW9FNYDRQ"  http://127.0.0.1:8080/users复制代码

结果:

{
  "result":["freewolf","tom","jerry"],
  "message":"",
  "status":0
}复制代码

说明我们的Token生效可以正常访问。其他的结果您可以自己去测试。再回到处理流程:

  • 接到请求进行拦截 - JWTAuthenticationFilter 中的方法
  • 验证JWT - TokenAuthenticationService 类 getAuthentication 方法
  • 访问Controller

这样本文的主要流程就结束了,本文主要介绍了,如何用Spring Security结合JWT保护你的Spring Boot应用。如何使用RoleAuthority,这里多说一句其实在Spring Security中,对于GrantedAuthority接口实现类来说是不区分是Role还是Authority,二者区别就是如果是hasAuthority判断,就是判断整个字符串,判断hasRole时,系统自动加上ROLE_到判断的Role字符串上,也就是说hasRole("CREATE")hasAuthority('ROLE_CREATE')是相同的。利用这些可以搭建完整的RBAC体系。本文到此,你已经会用了本文介绍的知识点。

使用 JWT 来保护你的 SpringBoot 应用的更多相关文章

  1. 用JWT来保护我们的ASP.NET Core Web API

    在上一篇博客中,自己动手写了一个Middleware来处理API的授权验证,现在就采用另外一种方式来处理这个授权验证的问题,毕竟现在也 有不少开源的东西可以用,今天用的是JWT. 什么是JWT呢?JW ...

  2. 使用jwt来保护你的接口服务

    以前写过一篇关于接口服务规范的文章,原文在此,里面关于安全性问题重点讲述了通过appid,appkey,timestamp,nonce以及sign来获取token,使用token来保障接口服务的安全. ...

  3. SpringBoot系列之前后端接口安全技术JWT

    @ 目录 1. 什么是JWT? 2. JWT令牌结构怎么样? 2.1 标头(Header) 2.2 有效载荷(Playload) 2.3 签名(Signature) 3. JWT原理简单介绍 4. J ...

  4. SpringBoot集成JWT实现权限认证

    目录 一.JWT认证流程 二.SpringBoot整合JWT 三.测试 上一篇文章<一分钟带你了解JWT认证!>介绍了JWT的组成和认证原理,本文将介绍下SpringBoot整合JWT实现 ...

  5. 【原创】SpringBoot & SpringCloud 快速入门学习笔记(完整示例)

    [原创]SpringBoot & SpringCloud 快速入门学习笔记(完整示例) 1月前在系统的学习SpringBoot和SpringCloud,同时整理了快速入门示例,方便能针对每个知 ...

  6. 使用JWT来实现对API的授权访问

    目录 什么是JWT JWT的结构 Header Payload Signature 解码后的JWT JWT是怎样工作的 在JAVA里使用JWT 引入依赖 JWT Service 生成JWT 解码JWT ...

  7. 基于Shiro,JWT实现微信小程序登录完整例子

    小程序官方流程图如下,官方地址 : https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html ...

  8. jwt的原理以及使用

    jwt原理(json web token) 我们之前是使用session实现登录,通过实际密码+盐组成字符串进行md5存入redis或者数据库中,输入的密码与实际校验通过,发送给客户端一个有效时间的t ...

  9. 小D课堂-SpringBoot 2.x微信支付在线教育网站项目实战_汇总

    2018年Spring Boot 2.x整合微信支付在线教育网站高级项目实战视频课程 小D课堂-SpringBoot 2.x微信支付在线教育网站项目实战_1-1.SpringBoot整合微信支付开发在 ...

随机推荐

  1. 带分数--第四届蓝桥杯省赛C++B/C组

    第四届蓝桥杯省赛C++B/C组----带分数 思路: 1.先枚举全排列 2.枚举位数 3.判断是否满足要求 这道题也就是n=a+b/c,求出符合要求的abc的方案数.进行优化时,可以对等式进行改写,改 ...

  2. JMM之Java线程间通讯——等待通知机制及其经典范式

    在并发编程中,实际处理涉及两个关键问题:线程之间如何通信及线程之间如何同步(这里的线程是指并发执行的活动实体). 通信是指线程之间以何种机制来交换信息.在共享内存的并发模型里,线程之间共享程序的公共状 ...

  3. pytest(12)-Allure常用特性allure.attach、allure.step、fixture、environment、categories

    上一篇文章pytest Allure生成测试报告我们学习了Allure中的一些特性,接下来继续学习其他常用的特性. allure.attach allure.attach用于在测试报告中添加附件,补充 ...

  4. 理解OAuth2.0协议和授权机制

    无论是自然资源还是互联网上的资源,需要控制使用权与被使用权,以保护资源的安全.合理的使用和有效的管控. 项目中,我们需要控制的是用户资源,既要保证有效用户的合理使用,又要防范非法用户的攻击.如此,如何 ...

  5. 攻防世界之Web_php_unserialize

    题目: <?php class Demo {     private $file = 'index.php';    public function __construct($file) {   ...

  6. ASP.NET Core 6框架揭秘实例演示[10]:Options基本编程模式

    依赖注入使我们可以将依赖的功能定义成服务,最终以一种松耦合的形式注入消费该功能的组件或者服务中.除了可以采用依赖注入的形式消费承载某种功能的服务,还可以采用相同的方式消费承载配置数据的Options对 ...

  7. Java笔记——循环语句

    Java笔记--循环语句     1. while语句 规律: 1. 首先计算表达式的值. 2. 若表达式为真,则执行循环语法,直至表达式为假,循环结束.   while(表达式) 语句; 例如: i ...

  8. 【C#反射】BindingFlags 枚举

    BindingFlags 枚举用途:Type的类方法中,用于筛选成员. type.InvokeMember方法中 type.GetConstructor 方法中 type.GetFiles方法中 ty ...

  9. 《Java编程思想》学习笔记(详细)

    目录 01 对象导论 02 一切都是对象 03 操作符 04 控制执行流程 05 初始化与清理 06 访问权限控制 07 复用类(继承) 08 多态 09 接口 10 内部类 11 持有对象 12 通 ...

  10. 详解pandas的read_csv方法

    楔子 使用pandas做数据处理的第一步就是读取数据,数据源可以来自于各种地方,csv文件便是其中之一.而读取csv文件,pandas也提供了非常强力的支持,参数有四五十个.这些参数中,有的很容易被忽 ...