重拾后端之Spring Boot(四):使用JWT和Spring Security保护REST API

重拾后端之Spring Boot(一):REST API的搭建能够这样简单
重拾后端之Spring Boot(二):MongoDb的无缝集成
重拾后端之Spring Boot(三):找回熟悉的Controller,Service
重拾后端之Spring Boot(四):使用 JWT 和 Spring Security 保护 REST APIjavascript

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

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

第一种就不介绍了,因为依赖Session来维护状态,也不太适合移动时代,新的项目就不要采用了。第二种OAuth的方案和JWT都是基于Token的,但OAuth其实对于不作开放平台的公司有些过于复杂。咱们主要介绍第三种:JWT。html

什么是JWT?

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

JWT的工做流程

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

  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工做流程图

为了更好的理解这个token是什么,咱们先来看一个token生成后的样子,下面那坨乱糟糟的就是了。github

eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ3YW5nIiwiY3JlYXRlZCI6MTQ4OTA3OTk4MTM5MywiZXhwIjoxNDg5Njg0NzgxfQ.RC-BYCe_UZ2URtWddUpWXIp4NMsoeq2O6UF-8tVplqXY1-CI9u1-a-9DAAJGfNWkHE81mpnR3gXzfrBAB3WUAg复制代码

但仔细看到的话仍是能够看到这个token分红了三部分,每部分用 . 分隔,每段都是用 Base64 编码的。若是咱们用一个Base64的解码器的话 ( www.base64decode.org/ ),能够看到第一部分 eyJhbGciOiJIUzUxMiJ9 被解析成了: web

{
    "alg":"HS512"
}复制代码

这是告诉咱们HMAC采用HS512算法对JWT进行的签名。算法

第二部分 eyJzdWIiOiJ3YW5nIiwiY3JlYXRlZCI6MTQ4OTA3OTk4MTM5MywiZXhwIjoxNDg5Njg0NzgxfQ 被解码以后是 spring

{
    "sub":"wang",
    "created":1489079981393,
    "exp":1489684781
}复制代码

这段告诉咱们这个Token中含有的数据声明(Claim),这个例子里面有三个声明:sub, createdexp。在咱们这个例子中,分别表明着用户名、建立时间和过时时间,固然你能够把任意数据声明在这里。mongodb

看到这里,你可能会想这是个什么鬼token,全部信息都透明啊,安全怎么保障?别急,咱们看看token的第三段 RC-BYCe_UZ2URtWddUpWXIp4NMsoeq2O6UF-8tVplqXY1-CI9u1-a-9DAAJGfNWkHE81mpnR3gXzfrBAB3WUAg。一样使用Base64解码以后,咦,这是什么东东

D X    DmYTeȧLUZcPZ0$gZAY_7wY@复制代码

最后一段实际上是签名,这个签名必须知道秘钥才能计算。这个也是JWT的安全保障。这里提一点注意事项,因为数据声明(Claim)是公开的,千万不要把密码等敏感字段放进去,不然就等因而公开给别人了。

也就是说JWT是由三段组成的,按官方的叫法分别是header(头)、payload(负载)和signature(签名):

header.payload.signature复制代码

头中的数据一般包含两部分:一个是咱们刚刚看到的 alg,这个词是 algorithm 的缩写,就是指明算法。另外一个能够添加的字段是token的类型(按RFC 7519实现的token机制不仅JWT一种),但若是咱们采用的是JWT的话,指定这个就多余了。

{
  "alg": "HS512",
  "typ": "JWT"
}复制代码

payload中能够放置三类数据:系统保留的、公共的和私有的:

  • 系统保留的声明(Reserved claims):这类声明不是必须的,可是是建议使用的,包括:iss (签发者), exp (过时时间),
    sub (主题), aud (目标受众)等。这里咱们发现都用的缩写的三个字符,这是因为JWT的目标就是尽量小巧。
  • 公共声明:这类声明须要在 IANA JSON Web Token Registry 中定义或者提供一个URI,由于要避免重名等冲突。
  • 私有声明:这个就是你根据业务须要本身定义的数据了。

签名的过程是这样的:采用header中声明的算法,接受三个参数:base64编码的header、base64编码的payload和秘钥(secret)进行运算。签名这一部分若是你愿意的话,能够采用RSASHA256的方式进行公钥、私钥对的方式进行,若是安全性要求的高的话。

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)复制代码

JWT的生成和解析

为了简化咱们的工做,这里引入一个比较成熟的JWT类库,叫 jjwt ( github.com/jwtk/jjwt )。这个类库能够用于Java和Android的JWT token的生成和验证。

JWT的生成可使用下面这样的代码完成:

String generateToken(Map<String, Object> claims) {
    return Jwts.builder()
            .setClaims(claims)
            .setExpiration(generateExpirationDate())
            .signWith(SignatureAlgorithm.HS512, secret) //采用什么算法是能够本身选择的,不必定非要采用HS512
            .compact();
}复制代码

数据声明(Claim)其实就是一个Map,好比咱们想放入用户名,能够简单的建立一个Map而后put进去就能够了。

Map<String, Object> claims = new HashMap<>();
claims.put(CLAIM_KEY_USERNAME, username());复制代码

解析也很简单,利用 jjwt 提供的parser传入秘钥,而后就能够解析token了。

Claims getClaimsFromToken(String token) {
    Claims claims;
    try {
        claims = Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();
    } catch (Exception e) {
        claims = null;
    }
    return claims;
}复制代码

JWT自己没啥难度,但安全总体是一个比较复杂的事情,JWT只不过提供了一种基于token的请求验证机制。但咱们的用户权限,对于API的权限划分、资源的权限划分,用户的验证等等都不是JWT负责的。也就是说,请求验证后,你是否有权限看对应的内容是由你的用户角色决定的。因此咱们这里要利用Spring的一个子项目Spring Security来简化咱们的工做。

Spring Security

Spring Security是一个基于Spring的通用安全框架,里面内容太多了,本文的主要目的也不是展开讲这个框架,而是如何利用Spring Security和JWT一块儿来完成API保护。因此关于Spring Secruity的基础内容或展开内容,请自行去官网学习( projects.spring.io/spring-secu… )。

简单的背景知识

若是你的系统有用户的概念的话,通常来讲,你应该有一个用户表,最简单的用户表,应该有三列:Id,Username和Password,相似下表这种

ID USERNAME PASSWORD
10 wang abcdefg

并且不是全部用户都是一种角色,好比网站管理员、供应商、财务等等,这些角色和网站的直接用户须要的权限多是不同的。那么咱们就须要一个角色表:

ID ROLE
10 USER
20 ADMIN

固然咱们还须要一个能够将用户和角色关联起来创建映射关系的表。

USER_ID ROLE_ID
10 10
20 20

这是典型的一个关系型数据库的用户角色的设计,因为咱们要使用的MongoDB是一个文档型数据库,因此让咱们从新审视一下这个结构。

这个数据结构的优势在于它避免了数据的冗余,每一个表负责本身的数据,经过关联表进行关系的描述,同时也保证的数据的完整性:好比当你修改角色名称后,没有脏数据的产生。

可是这种事情在用户权限这个领域发生的频率到底有多少呢?有多少人天天不停的改的角色名称?固然若是你的业务场景确实是须要保证数据完整性,你仍是应该使用关系型数据库。但若是没有高频的对于角色表的改动,其实咱们是不须要这样的一个设计的。在MongoDB中咱们能够将其简化为

{
  _id: <id_generated>
  username: 'user',
  password: 'pass',
  roles: ['USER', 'ADMIN']
}复制代码

基于以上考虑,咱们重构一下 User 类,

@Data
public class User {
    @Id
    private String id;

    @Indexed(unique=true, direction= IndexDirection.DESCENDING, dropDups=true)
    private String username;

    private String password;
    private String email;
    private Date lastPasswordResetDate;
    private List<String> roles;
}复制代码

固然你可能发现这个类有点怪,只有一些field,这个简化的能力是一个叫lombok类库提供的 ,这个不少开发过Android的童鞋应该熟悉,是用来简化POJO的建立的一个类库。简单说一下,采用 lombok 提供的 @Data 修饰符后能够简写成,原来的一坨getter和setter以及constructor等都不须要写了。相似的 Todo 能够改写成:

@Data
public class Todo {
    @Id private String id;
    private String desc;
    private boolean completed;
    private User user;
}复制代码

增长这个类库只需在 build.gradle 中增长下面这行

dependencies {
    // 省略其它依赖
    compile("org.projectlombok:lombok:${lombokVersion}")
}复制代码

引入Spring Security

要在Spring Boot中引入Spring Security很是简单,修改 build.gradle,增长一个引用 org.springframework.boot:spring-boot-starter-security

dependencies {
    compile("org.springframework.boot:spring-boot-starter-data-rest")
    compile("org.springframework.boot:spring-boot-starter-data-mongodb")
    compile("org.springframework.boot:spring-boot-starter-security")
    compile("io.jsonwebtoken:jjwt:${jjwtVersion}")
    compile("org.projectlombok:lombok:${lombokVersion}")
    testCompile("org.springframework.boot:spring-boot-starter-test")
}复制代码

你可能发现了,咱们不仅增长了对Spring Security的编译依赖,还增长 jjwt 的依赖。

Spring Security须要咱们实现几个东西,第一个是UserDetails:这个接口中规定了用户的几个必需要有的方法,因此咱们建立一个JwtUser类来实现这个接口。为何不直接使用User类?由于这个UserDetails彻底是为了安全服务的,它和咱们的领域类可能有部分属性重叠,但不少的接口实际上是安全定制的,因此最好新建一个类:

public class JwtUser implements UserDetails {
    private final String id;
    private final String username;
    private final String password;
    private final String email;
    private final Collection<? extends GrantedAuthority> authorities;
    private final Date lastPasswordResetDate;

    public JwtUser( String id, String username, String password, String email, Collection<? extends GrantedAuthority> authorities, Date lastPasswordResetDate) {
        this.id = id;
        this.username = username;
        this.password = password;
        this.email = email;
        this.authorities = authorities;
        this.lastPasswordResetDate = lastPasswordResetDate;
    }
    //返回分配给用户的角色列表
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

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

    @JsonIgnore
    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return 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 true;
    }
    // 这个是自定义的,返回上次密码重置日期
    @JsonIgnore
    public Date getLastPasswordResetDate() {
        return lastPasswordResetDate;
    }
}复制代码

这个接口中规定的不少方法咱们都简单粗暴的设成直接返回某个值了,这是为了简单起见,你在实际开发环境中仍是要根据具体业务调整。固然因为两个类仍是有必定关系的,为了写起来简单,咱们写一个工厂类来由领域对象建立 JwtUser,这个工厂就叫 JwtUserFactory 吧:

public final class JwtUserFactory {

    private JwtUserFactory() {
    }

    public static JwtUser create(User user) {
        return new JwtUser(
                user.getId(),
                user.getUsername(),
                user.getPassword(),
                user.getEmail(),
                mapToGrantedAuthorities(user.getRoles()),
                user.getLastPasswordResetDate()
        );
    }

    private static List<GrantedAuthority> mapToGrantedAuthorities(List<String> authorities) {
        return authorities.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
    }
}复制代码

第二个要实现的是 UserDetailsService,这个接口只定义了一个方法 loadUserByUsername,顾名思义,就是提供一种从用户名能够查到用户并返回的方法。注意,不必定是数据库哦,文本文件、xml文件等等均可能成为数据源,这也是为何Spring提供这样一个接口的缘由:保证你能够采用灵活的数据源。接下来咱们创建一个 JwtUserDetailsServiceImpl 来实现这个接口。

@Service
public class JwtUserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username);

        if (user == null) {
            throw new UsernameNotFoundException(String.format("No user found with username '%s'.", username));
        } else {
            return JwtUserFactory.create(user);
        }
    }
}复制代码

为了让Spring能够知道咱们想怎样控制安全性,咱们还须要创建一个安全配置类 WebSecurityConfig

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{

    // Spring会自动寻找一样类型的具体类注入,这里就是JwtUserDetailsServiceImpl了
    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        authenticationManagerBuilder
                // 设置UserDetailsService
                .userDetailsService(this.userDetailsService)
                // 使用BCrypt进行密码的hash
                .passwordEncoder(passwordEncoder());
    }
    // 装载BCrypt密码编码器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                // 因为使用的是JWT,咱们这里不须要csrf
                .csrf().disable()

                // 基于token,因此不须要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()

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

                // 容许对于网站静态资源的无受权访问
                .antMatchers(
                        HttpMethod.GET,
                        "/",
                        "/*.html",
                        "/favicon.ico",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js"
                ).permitAll()
                // 对于获取token的rest api要容许匿名访问
                .antMatchers("/auth/**").permitAll()
                // 除上面外的全部请求所有须要鉴权认证
                .anyRequest().authenticated();

        // 禁用缓存
        httpSecurity.headers().cacheControl();
    }
}复制代码

接下来咱们要规定一下哪些资源须要什么样的角色能够访问了,在 UserController 加一个修饰符 @PreAuthorize("hasRole('ADMIN')") 表示这个资源只能被拥有 ADMIN 角色的用户访问。

/** * 在 @PreAuthorize 中咱们能够利用内建的 SPEL 表达式:好比 'hasRole()' 来决定哪些用户有权访问。 * 需注意的一点是 hasRole 表达式认为每一个角色名字前都有一个前缀 'ROLE_'。因此这里的 'ADMIN' 其实在 * 数据库中存储的是 'ROLE_ADMIN' 。这个 @PreAuthorize 能够修饰Controller也可修饰Controller中的方法。 **/
@RestController
@RequestMapping("/users")
@PreAuthorize("hasRole('ADMIN')")
public class UserController {
    @Autowired
    private UserRepository repository;

    @RequestMapping(method = RequestMethod.GET)
    public List<User> getUsers() {
        return repository.findAll();
    }

    // 略去其它部分
}复制代码

相似的咱们给 TodoController 加上 @PreAuthorize("hasRole('USER')"),标明这个资源只能被拥有 USER 角色的用户访问:

@RestController
@RequestMapping("/todos")
@PreAuthorize("hasRole('USER')")
public class TodoController {
    // 略去
}复制代码

使用application.yml配置SpringBoot应用

如今应该Spring Security能够工做了,但为了能够更清晰的看到工做日志,咱们但愿配置一下,在和 src 同级创建一个config文件夹,在这个文件夹下面新建一个 application.yml

# Server configuration
server:
 port: 8090
 contextPath:

# Spring configuration
spring:
 jackson:
 serialization:
 INDENT_OUTPUT: true
  data.mongodb:
 host: localhost
 port: 27017
 database: springboot

# Logging configuration
logging:
 level:
    org.springframework:
 data: DEBUG
 security: DEBUG复制代码

咱们除了配置了logging的一些东东外,也顺手设置了数据库和http服务的一些配置项,如今咱们的服务器会在8090端口监听,而spring data和security的日志在debug模式下会输出到console。

如今启动服务后,访问 http://localhost:8090 你能够看到根目录仍是正常显示的

根目录仍是正常能够访问的

但咱们试一下 http://localhost:8090/users ,观察一下console,咱们会看到以下的输出,告诉因为用户未鉴权,咱们访问被拒绝了。

2017-03-10 15:51:53.351 DEBUG 57599 --- [nio-8090-exec-4] o.s.s.w.a.ExceptionTranslationFilter     : Access is denied (user is anonymous); redirecting to authentication entry point

org.springframework.security.access.AccessDeniedException: Access is denied
    at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:84) ~[spring-security-core-4.2.1.RELEASE.jar:4.2.1.RELEASE]复制代码

集成JWT和Spring Security

到如今,咱们仍是让JWT和Spring Security各自为战,并无集成起来。要想要JWT在Spring中工做,咱们应该新建一个filter,并把它配置在 WebSecurityConfig 中。

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Value("${jwt.header}")
    private String tokenHeader;

    @Value("${jwt.tokenHead}")
    private String tokenHead;

    @Override
    protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        String authHeader = request.getHeader(this.tokenHeader);
        if (authHeader != null && authHeader.startsWith(tokenHead)) {
            final String authToken = authHeader.substring(tokenHead.length()); // The part after "Bearer "
            String username = jwtTokenUtil.getUsernameFromToken(authToken);

            logger.info("checking authentication " + username);

            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {

                UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);

                if (jwtTokenUtil.validateToken(authToken, userDetails)) {
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                            userDetails, null, userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(
                            request));
                    logger.info("authenticated user " + username + ", setting security context");
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }

        chain.doFilter(request, response);
    }
}复制代码

事实上若是咱们足够相信token中的数据,也就是咱们足够相信签名token的secret的机制足够好,这种状况下,咱们能够不用再查询数据库,而直接采用token中的数据。本例中,咱们仍是经过Spring Security的 @UserDetailsService 进行了数据查询,但简单验证的话,你能够采用直接验证token是否合法来避免昂贵的数据查询。

接下来,咱们会在 WebSecurityConfig 中注入这个filter,而且配置到 HttpSecurity 中:

public class WebSecurityConfig extends WebSecurityConfigurerAdapter{

    // 省略其它部分

    @Bean
    public JwtAuthenticationTokenFilter authenticationTokenFilterBean() throws Exception {
        return new JwtAuthenticationTokenFilter();
    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        // 省略以前写的规则部分,具体看前面的代码

        // 添加JWT filter
        httpSecurity
                .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
    }
}复制代码

完成鉴权(登陆)、注册和更新token的功能

到如今,咱们整个API其实已经在安全的保护下了,但咱们遇到一个问题:全部的API都安全了,但咱们尚未用户啊,因此全部API都无法访问。所以要提供一个注册、登陆的API,这个API应该是能够匿名访问的。给它规划的路径呢,咱们前面其实在WebSecurityConfig中已经给出了,就是 /auth

首先须要一个AuthService,规定一下必选动做:

public interface AuthService {
    User register(User userToAdd);
    String login(String username, String password);
    String refresh(String oldToken);
}复制代码

而后,实现这些必选动做,其实很是简单:

  1. 登陆时要生成token,完成Spring Security认证,而后返回token给客户端
  2. 注册时将用户密码用BCrypt加密,写入用户角色,因为是开放注册,因此写入角色系统控制,将其写成 ROLE_USER
  3. 提供一个能够刷新token的接口 refresh 用于取得新的token
@Service
public class AuthServiceImpl implements AuthService {

    private AuthenticationManager authenticationManager;
    private UserDetailsService userDetailsService;
    private JwtTokenUtil jwtTokenUtil;
    private UserRepository userRepository;

    @Value("${jwt.tokenHead}")
    private String tokenHead;

    @Autowired
    public AuthServiceImpl( AuthenticationManager authenticationManager, UserDetailsService userDetailsService, JwtTokenUtil jwtTokenUtil, UserRepository userRepository) {
        this.authenticationManager = authenticationManager;
        this.userDetailsService = userDetailsService;
        this.jwtTokenUtil = jwtTokenUtil;
        this.userRepository = userRepository;
    }

    @Override
    public User register(User userToAdd) {
        final String username = userToAdd.getUsername();
        if(userRepository.findByUsername(username)!=null) {
            return null;
        }
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        final String rawPassword = userToAdd.getPassword();
        userToAdd.setPassword(encoder.encode(rawPassword));
        userToAdd.setLastPasswordResetDate(new Date());
        userToAdd.setRoles(asList("ROLE_USER"));
        return userRepository.insert(userToAdd);
    }

    @Override
    public String login(String username, String password) {
        UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken(username, password);
        final Authentication authentication = authenticationManager.authenticate(upToken);
        SecurityContextHolder.getContext().setAuthentication(authentication);

        final UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        final String token = jwtTokenUtil.generateToken(userDetails);
        return token;
    }

    @Override
    public String refresh(String oldToken) {
        final String token = oldToken.substring(tokenHead.length());
        String username = jwtTokenUtil.getUsernameFromToken(token);
        JwtUser user = (JwtUser) userDetailsService.loadUserByUsername(username);
        if (jwtTokenUtil.canTokenBeRefreshed(token, user.getLastPasswordResetDate())){
            return jwtTokenUtil.refreshToken(token);
        }
        return null;
    }
}复制代码

而后创建AuthController就好,这个AuthController中咱们在其中使用了表达式绑定,好比 @Value("${jwt.header}")中的 jwt.header 实际上是定义在 applicaiton.yml 中的

# JWT
jwt:
 header: Authorization
 secret: mySecret
 expiration: 604800
 tokenHead: "Bearer "
 route:
 authentication:
 path: auth
 refresh: refresh
 register: "auth/register"复制代码

一样的 @RequestMapping(value = "${jwt.route.authentication.path}", method = RequestMethod.POST) 中的 jwt.route.authentication.path 也是定义在上面的

@RestController
public class AuthController {
    @Value("${jwt.header}")
    private String tokenHeader;

    @Autowired
    private AuthService authService;

    @RequestMapping(value = "${jwt.route.authentication.path}", method = RequestMethod.POST)
    public ResponseEntity<?> createAuthenticationToken(
            @RequestBody JwtAuthenticationRequest authenticationRequest) throws AuthenticationException{
        final String token = authService.login(authenticationRequest.getUsername(), authenticationRequest.getPassword());

        // Return the token
        return ResponseEntity.ok(new JwtAuthenticationResponse(token));
    }

    @RequestMapping(value = "${jwt.route.authentication.refresh}", method = RequestMethod.GET)
    public ResponseEntity<?> refreshAndGetAuthenticationToken(
            HttpServletRequest request) throws AuthenticationException{
        String token = request.getHeader(tokenHeader);
        String refreshedToken = authService.refresh(token);
        if(refreshedToken == null) {
            return ResponseEntity.badRequest().body(null);
        } else {
            return ResponseEntity.ok(new JwtAuthenticationResponse(refreshedToken));
        }
    }

    @RequestMapping(value = "${jwt.route.authentication.register}", method = RequestMethod.POST)
    public User register(@RequestBody User addedUser) throws AuthenticationException{
        return authService.register(addedUser);
    }
}复制代码

验证时间

接下来,咱们就能够看看咱们的成果了,首先注册一个用户 peng2,很完美的注册成功了

注册用户

而后在 /auth 中取得token,也很成功

取得token

不使用token时,访问 /users 的结果,不出意料的失败,提示未受权。

不使用token访问users列表

使用token时,访问 /users 的结果,虽然还是失败,但此次提示访问被拒绝,意思就是虽然你已经获得了受权,但因为你的会员级别还只是普卡会员,因此你的请求被拒绝。

image_1bas22va52vk1rj445fhm87k72a.png-156.9kB

接下来咱们访问 /users/?username=peng2,居然能够访问啊

访问本身的信息是容许的

这是因为咱们为这个方法定义的权限就是:拥有ADMIN角色或者是当前用户自己。Spring Security真是很方便,很强大。

@PostAuthorize("returnObject.username == principal.username or hasRole('ROLE_ADMIN')")
    @RequestMapping(value = "/",method = RequestMethod.GET)
    public User getUserByUsername(@RequestParam(value="username") String username) {
        return repository.findByUsername(username);
    }复制代码

本章代码: github.com/wpcfan/spri…

重拾后端之Spring Boot(一):REST API的搭建能够这样简单
重拾后端之Spring Boot(二):MongoDb的无缝集成
重拾后端之Spring Boot(三):找回熟悉的Controller,Service
重拾后端之Spring Boot(四):使用 JWT 和 Spring Security 保护 REST API

相关文章
相关标签/搜索