通常情况下,把API直接暴露出去是风险很大的,不说别的,直接被机器攻击就喝一壶的。那么一般来说,对API要划分出一定的权限级别,然后做一个用户的鉴权,依据鉴权结果给予用户开放对应的API。目前,比较主流的方案有几种:
用户名和密码鉴权,使用Session保存用户鉴权结果。
使用OAuth进行鉴权(其实OAuth也是一种基于Token的鉴权,只是没有规定Token的生成方式)
自行采用Token进行鉴权。

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

什么是JWT?

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

JWT的工作流程

  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. 流程图如下:

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

但仔细看到的话还是可以看到这个token分成了三部分,每部分用 . 分隔,每段都是用 Base64 编码的。如果我们用一个Base64的解码器的话 ( https://www.base64decode.org/ ),可以看到第一部分 eyJhbGciOiJIUzUxMiJ9 被解析成了:

{
"alg":"HS512"
}

这是告诉我们HMAC采用HS512算法对JWT进行的签名。
第二部分 eyJzdWIiOiJ3YW5naGFvIiwiY3JlYXRlZCI6MTUwNDc1MTU2MjE2MywiZXhwIjoxNTA1MzU2MzYyfQ被解码之后是

{"sub":"wanghao","created":1504751562163,"exp":1505356362}

这段告诉我们这个Token中含有的数据声明(Claim),这个例子里面有三个声明:sub, created 和 exp。在我们这个例子中,分别代表着用户名、创建时间和过期时间,当然你可以把任意数据声明在这里。
看到这里,你可能会想这是个什么鬼token,所有信息都透明啊,安全怎么保障?别急,我们看看token的第三段 4SLVNnfDLABZBEBglSD2iLVtIRpF43aSzLvnY8viizLFDM3b4c8vf_XAm_cMUoFsta5iyOwkH49aqz8m6tmlFA。同样使用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。这个类库可以用于Java和Android的JWT token的生成和验证。

<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.7.0</version>
</dependency>
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

由于我们要使用的MongoDB是一个文档型数据库,
{
_id: <id_generated>
username: 'user',
password: 'pass',
roles: ['USER', 'ADMIN']
}

基于以上考虑,我们建 User 类,

package com.easted.card.core.entity;import java.util.Date;
import java.util.List;import javax.persistence.Entity;import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.index.IndexDirection;
import org.springframework.data.mongodb.core.index.Indexed;@Entity
public class User {@Idprivate 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;/*** @Title: getId <BR>* @return:String <BR>*/public String getId() {return id;}/*** @param id the id to set*/public void setId(String id) {this.id = id;}/*** @Title: getUsername <BR>* @return:String <BR>*/public String getUsername() {return username;}/*** @param username the username to set*/public void setUsername(String username) {this.username = username;}/*** @Title: getPassword <BR>* @return:String <BR>*/public String getPassword() {return password;}/*** @param password the password to set*/public void setPassword(String password) {this.password = password;}/*** @Title: getEmail <BR>* @return:String <BR>*/public String getEmail() {return email;}/*** @param email the email to set*/public void setEmail(String email) {this.email = email;}/*** @Title: getLastPasswordResetDate <BR>* @return:Date <BR>*/public Date getLastPasswordResetDate() {return lastPasswordResetDate;}/*** @param lastPasswordResetDate the lastPasswordResetDate to set*/public void setLastPasswordResetDate(Date lastPasswordResetDate) {this.lastPasswordResetDate = lastPasswordResetDate;}/*** @Title: getRoles <BR>* @return:List<String> <BR>*/public List<String> getRoles() {return roles;}/*** @param roles the roles to set*/public void setRoles(List<String> roles) {this.roles = roles;}
}

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.easted</groupId><artifactId>card</artifactId><version>0.0.1-SNAPSHOT</version><packaging>jar</packaging><name>card</name><description>Demo project for Spring Boot</description><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>1.5.6.RELEASE</version><relativePath /> <!-- lookup parent from repository --></parent><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><java.version>1.8</java.version><spring-cloud.version>Dalston.SR3</spring-cloud.version></properties><dependencies><dependency><groupId>org.springframework.security.oauth</groupId><artifactId>spring-security-oauth2</artifactId></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-jwt</artifactId></dependency><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId><scope>compile</scope></dependency><!--    <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency> --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-mongodb</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-mail</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.postgresql</groupId><artifactId>postgresql</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-test</artifactId><scope>test</scope></dependency><!-- lombok 在类上加@Data可以省略get、set方法 --><!-- <dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><scope>provided</scope></dependency> --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.7.0</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-rest</artifactId></dependency><dependency><groupId>org.eclipse.persistence</groupId><artifactId>javax.persistence</artifactId><version>2.0.0</version></dependency></dependencies><dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>${spring-cloud.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>

application.yml

# Server configuration
server:port: 8090contextPath:# Spring configuration
spring:jackson:serialization:INDENT_OUTPUT: truedata:  mongodb:  uri: mongodb://127.0.0.1:27017/springboot# JWT
jwt:header: Authorizationsecret: mySecretexpiration: 604800tokenHead: "Bearer"route:authentication:path: authrefresh: refreshregister: "auth/register"# Logging configuration
logging:level:org.springframework:data: DEBUGsecurity: DEBUG

Spring Security需要我们实现几个东西,第一个是UserDetails:这个接口中规定了用户的几个必须要有的方法,所以我们创建一个JwtUser类来实现这个接口。为什么不直接使用User类?因为这个UserDetails完全是为了安全服务的,它和我们的领域类可能有部分属性重叠,但很多的接口其实是安全定制的,所以最好新建一个类:

package com.easted.card.core.security;import java.util.Collection;
import java.util.Date;import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;import com.fasterxml.jackson.annotation.JsonIgnore;public class JwtUser implements UserDetails {/**  * @Fields serialVersionUID : serialVersionUID*/  private static final long serialVersionUID = 1L;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;}//返回分配给用户的角色列表@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return authorities;}@JsonIgnorepublic String getId() {return id;}@JsonIgnore@Overridepublic String getPassword() {return password;}@Overridepublic String getUsername() {return username;}// 账户是否未过期@JsonIgnore@Overridepublic boolean isAccountNonExpired() {return true;}// 账户是否未锁定@JsonIgnore@Overridepublic boolean isAccountNonLocked() {return true;}// 密码是否未过期@JsonIgnore@Overridepublic boolean isCredentialsNonExpired() {return true;}// 账户是否激活@JsonIgnore@Overridepublic boolean isEnabled() {return true;}//返回上次密码重置日期@JsonIgnorepublic Date getLastPasswordResetDate() {return lastPasswordResetDate;}/*** @Title: getEmail <BR>* @return:String <BR>*/public String getEmail() {return email;}}

由于两个类还是有一定关系的,为了写起来简单,我们写一个工厂类来由领域对象创建 JwtUser,这个工厂就叫 JwtUserFactory

package com.easted.card.core.security;import java.util.List;
import java.util.stream.Collectors;import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;import com.easted.card.core.entity.User;
/*** User工厂类* @ClassName:JwtUserFactory* @author:Wanghao* @date: 2017年9月6日 下午4:01:35*/
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提供这样一个接口的原因:保证你可以采用灵活的数据源。接下来我们建立一个 CustomUserService来实现这个接口。


package com.easted.card.core.security;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;import com.easted.card.core.entity.User;
import com.easted.card.core.repository.UserRepository;/*** 自定义UserService需实现UserDetailsService接口。可直接返回给springSecurity使用。* @ClassName:CustomUserService* @author:Wanghao* @date: 2017年9月6日 下午4:04:18*/
@Service
public class CustomUserService implements UserDetailsService{@Autowiredprivate UserRepository userRepository; @Overridepublic 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:
集成JWT和Spring Security

package com.easted.card.core.config;import org.springframework.beans.factory.annotation.Autowired;
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;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;import com.easted.card.core.security.JwtAuthenticationEntryPoint;
import com.easted.card.core.security.JwtAuthenticationTokenFilter;@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{@Autowiredprivate JwtAuthenticationEntryPoint unauthorizedHandler;@Autowiredprivate UserDetailsService userDetailsService;@Autowiredpublic void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {authenticationManagerBuilder.userDetailsService(this.userDetailsService).passwordEncoder(passwordEncoder());}@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Beanpublic JwtAuthenticationTokenFilter authenticationTokenFilterBean() throws Exception {return new JwtAuthenticationTokenFilter();}@Overrideprotected void configure(HttpSecurity httpSecurity) throws Exception {httpSecurity// 由于使用的是JWT,我们这里不需要csrf.csrf().disable().exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()// 基于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();// 添加JWT filterhttpSecurity.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);// 禁用缓存httpSecurity.headers().cacheControl();}
}

JwtAuthenticationTokenFilter

package com.easted.card.core.security;import java.io.IOException;import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
/*** JWT filter* @ClassName:JwtAuthenticationTokenFilter* @author:Wanghao* @date: 2017年9月6日 下午4:59:42*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {@Resourceprivate UserDetailsService userDetailsService;@Autowiredprivate JwtTokenUtil jwtTokenUtil;@Value("${jwt.header}")private String tokenHeader;@Value("${jwt.tokenHead}")private String tokenHead;@Overrideprotected 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) {// 如果我们足够相信token中的数据,也就是我们足够相信签名token的secret的机制足够好// 这种情况下,我们可以不用再查询数据库,而直接采用token中的数据// 本例中,我们还是通过Spring Security的 @UserDetailsService 进行了数据查询// 但简单验证的话,你可以采用直接验证token是否合法来避免昂贵的数据查询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的功能

package com.easted.card.core.service;import com.easted.card.core.entity.User;public interface AuthService {User register(User userToAdd);String login(String username, String password);String refresh(String oldToken);
}

AuthServiceImpl

package com.easted.card.core.service.impl;import static java.util.Arrays.asList;import java.util.Date;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;import com.easted.card.core.entity.User;
import com.easted.card.core.repository.UserRepository;
import com.easted.card.core.security.JwtTokenUtil;
import com.easted.card.core.security.JwtUser;
import com.easted.card.core.service.AuthService;@Service
public class AuthServiceImpl implements AuthService {private AuthenticationManager authenticationManager;private UserDetailsService userDetailsService;private JwtTokenUtil jwtTokenUtil;private UserRepository userRepository;@Value("${jwt.tokenHead}")private String tokenHead;@Autowiredpublic AuthServiceImpl(AuthenticationManager authenticationManager,UserDetailsService userDetailsService,JwtTokenUtil jwtTokenUtil,UserRepository userRepository) {this.authenticationManager = authenticationManager;this.userDetailsService = userDetailsService;this.jwtTokenUtil = jwtTokenUtil;this.userRepository = userRepository;}@Overridepublic 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);}@Overridepublic String login(String username, String password) {UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken(username, password);// Perform the securityfinal Authentication authentication = authenticationManager.authenticate(upToken);SecurityContextHolder.getContext().setAuthentication(authentication);// Reload password post-security so we can generate tokenfinal UserDetails userDetails = userDetailsService.loadUserByUsername(username);final String token = jwtTokenUtil.generateToken(userDetails);return token;}@Overridepublic 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

package com.easted.card.web.controller;import javax.servlet.http.HttpServletRequest;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;import com.easted.card.core.entity.User;
import com.easted.card.core.security.JwtAuthenticationRequest;
import com.easted.card.core.security.JwtAuthenticationResponse;
import com.easted.card.core.service.AuthService;
/*** 权限验证控制器* @ClassName:AuthController* @author:Wanghao* @date: 2017年9月7日 上午10:26:35*/
@RestController
public class AuthController {@Value("${jwt.header}")private String tokenHeader;@Autowiredprivate 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 tokenreturn 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);}
}

JwtAuthenticationRequest

package com.easted.card.core.security;import java.io.Serializable;public class  JwtAuthenticationRequest implements Serializable {private static final long serialVersionUID = -8445943548965154778L;private String username;private String password;public JwtAuthenticationRequest() {super();}public JwtAuthenticationRequest(String username, String password) {this.setUsername(username);this.setPassword(password);}public String getUsername() {return this.username;}public void setUsername(String username) {this.username = username;}public String getPassword() {return this.password;}public void setPassword(String password) {this.password = password;}
}

标题

package com.easted.card.core.security;import java.io.Serializable;public class JwtAuthenticationResponse implements Serializable {private static final long serialVersionUID = 1250166508152483573L;private final String token;public JwtAuthenticationResponse(String token) {this.token = token;}public String getToken() {return this.token;}
}

JwtTokenUtil

package com.easted.card.core.security;import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;@Component
public class JwtTokenUtil implements Serializable {private static final long serialVersionUID = -3301605591108950415L;private static final String CLAIM_KEY_USERNAME = "sub";private static final String CLAIM_KEY_CREATED = "created";@Value("${jwt.secret}")private String secret;@Value("${jwt.expiration}")private Long expiration;public String getUsernameFromToken(String token) {String username;try {final Claims claims = getClaimsFromToken(token);username = claims.getSubject();} catch (Exception e) {username = null;}return username;}public Date getCreatedDateFromToken(String token) {Date created;try {final Claims claims = getClaimsFromToken(token);created = new Date((Long) claims.get(CLAIM_KEY_CREATED));} catch (Exception e) {created = null;}return created;}public Date getExpirationDateFromToken(String token) {Date expiration;try {final Claims claims = getClaimsFromToken(token);expiration = claims.getExpiration();} catch (Exception e) {expiration = null;}return expiration;}private Claims getClaimsFromToken(String token) {Claims claims;try {claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();} catch (Exception e) {claims = null;}return claims;}private Date generateExpirationDate() {return new Date(System.currentTimeMillis() + expiration * 1000);}private Boolean isTokenExpired(String token) {final Date expiration = getExpirationDateFromToken(token);return expiration.before(new Date());}private Boolean isCreatedBeforeLastPasswordReset(Date created, Date lastPasswordReset) {return (lastPasswordReset != null && created.before(lastPasswordReset));}public String generateToken(UserDetails userDetails) {Map<String, Object> claims = new HashMap<>();claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());claims.put(CLAIM_KEY_CREATED, new Date());return generateToken(claims);}String generateToken(Map<String, Object> claims) {return Jwts.builder().setClaims(claims).setExpiration(generateExpirationDate()).signWith(SignatureAlgorithm.HS512, secret).compact();}public Boolean canTokenBeRefreshed(String token, Date lastPasswordReset) {final Date created = getCreatedDateFromToken(token);return !isCreatedBeforeLastPasswordReset(created, lastPasswordReset)&& !isTokenExpired(token);}public String refreshToken(String token) {String refreshedToken;try {final Claims claims = getClaimsFromToken(token);claims.put(CLAIM_KEY_CREATED, new Date());refreshedToken = generateToken(claims);} catch (Exception e) {refreshedToken = null;}return refreshedToken;}public Boolean validateToken(String token, UserDetails userDetails) {JwtUser user = (JwtUser) userDetails;final String username = getUsernameFromToken(token);final Date created = getCreatedDateFromToken(token);//final Date expiration = getExpirationDateFromToken(token);return (username.equals(user.getUsername())&& !isTokenExpired(token)&& !isCreatedBeforeLastPasswordReset(created, user.getLastPasswordResetDate()));}
}

JwtAuthenticationEntryPoint

package com.easted.card.core.security;import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.Serializable;@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {private static final long serialVersionUID = -8970718410437077606L;@Overridepublic void commence(HttpServletRequest request,HttpServletResponse response,AuthenticationException authException) throws IOException {// This is invoked when user tries to access a secured REST resource without supplying any credentials// We should just send a 401 Unauthorized response because there is no 'login page' to redirect toresponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");}
}

UserRepository

package com.easted.card.core.repository;import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;import com.easted.card.core.entity.User;@Repository
public interface UserRepository extends MongoRepository<User, String> {User findByUsername(final String username);
}

UserController

package com.easted.card.web.controller;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;import com.easted.card.core.entity.User;
import com.easted.card.core.repository.UserRepository;import java.util.List;/*** 在 @PreAuthorize 中我们可以利用内建的 SPEL 表达式:比如 'hasRole()' 来决定哪些用户有权访问。* 需注意的一点是 hasRole 表达式认为每个角色名字前都有一个前缀 'ROLE_'。所以这里的 'ADMIN' 其实在* 数据库中存储的是 'ROLE_ADMIN' 。这个 @PreAuthorize 可以修饰Controller也可修饰Controller中的方法。**/
@RestController
@RequestMapping("/users")
public class UserController {@Autowiredprivate UserRepository repository;@PreAuthorize("hasRole('ADMIN')")@RequestMapping(method = RequestMethod.GET)public List<User> getUsers() {return repository.findAll();}@PreAuthorize("hasRole('ADMIN')")@RequestMapping(method = RequestMethod.POST)User addUser(@RequestBody User addedUser) {return repository.insert(addedUser);}@PostAuthorize("returnObject.username == principal.username or hasRole('ROLE_ADMIN')")@RequestMapping(value = "/{id}", method = RequestMethod.GET)public User getUser(@PathVariable String id) {return repository.findOne(id);}@PreAuthorize("hasRole('ADMIN')")@RequestMapping(value = "/{id}", method = RequestMethod.PUT)User updateUser(@PathVariable String id, @RequestBody User updatedUser) {updatedUser.setId(id);return repository.save(updatedUser);}@PreAuthorize("hasRole('ADMIN')")@RequestMapping(value = "/{id}", method = RequestMethod.DELETE)User removeUser(@PathVariable String id) {User deletedUser = repository.findOne(id);repository.delete(id);return deletedUser;}@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);}
}

验证

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

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

使用token时,访问 /users 的结果,虽然仍是失败,但这次提示访问被拒绝,意思就是虽然你已经得到了授权,但由于你的会员级别还只是普卡会员,所以你的请求被拒绝。

接下来我们访问 /users/?username=wah,竟然可以访问啊
这是由于我们为这个方法定义的权限就是:拥有ADMIN角色或者是当前用户本身。Spring Security真是很方便,很强大。

转载:http://www.jianshu.com/p/6307c89fe3fa

使用JWT和Spring Security保护REST API相关推荐

  1. tomcat使用ssl_使用SSL和Spring Security保护Tomcat应用程序的安全

    tomcat使用ssl 如果您看过我的上一个博客,您会知道我列出了Spring Security可以做的十件事 . 但是,在开始认真使用Spring Security之前,您真正要做的第一件事就是确保 ...

  2. 使用Spring Security保护REST服务

    总览 最近,我正在一个使用REST服务层与客户端应用程序(GWT应用程序)进行通信的项目中. 因此,我花了很多时间来弄清楚如何使用Spring Security保护REST服务. 本文介绍了我找到的解 ...

  3. 使用SSL和Spring Security保护Tomcat应用程序的安全

    如果您看过我的上一个博客,您会知道我列出了Spring Security可以做的十件事 . 但是,在认真开始使用Spring Security之前,您真正要做的第一件事就是确保您的Web应用使用正确的 ...

  4. 使用带有OAuth的Spring Security保护资源

    1.简介 在本教程中,我们将研究如何使用Spring Security和OAuth来基于路径模式( / api / ** )保护服务器上的管理资源. 我们配置的另一个路径模式( / oauth / t ...

  5. gwt格式_使用Spring Security保护GWT应用程序的安全

    gwt格式 在本教程中,我们将看到如何将GWT与Spring的安全模块(即Spring Security)集成. 我们将看到如何保护GWT入口点,如何检索用户的凭据以及如何记录各种身份验证事件. 此外 ...

  6. 使用Spring Security保护GWT应用程序

    在本教程中,我们将看到如何将GWT与Spring的安全模块(即Spring Security)集成在一起. 我们将看到如何保护GWT入口点,如何检索用户的凭据以及如何记录各种身份验证事件. 此外,我们 ...

  7. JWT实战 Spring Security Oauth2整合JWT 整合SSO单点登录

    文章目录 一.JWT 1.1 什么是JWT 1.2 JWT组成 头部(header) 载荷(payload) 签名(signature) 如何应用 1.3 JJWT 快速开始 创建token toke ...

  8. 将JWT与Spring Security OAuth结合使用

    1.概述 在本教程中,我们将讨论如何使用Spring Security OAuth2实现来使用JSON Web令牌. 我们还将继续构建此OAuth系列的上一篇文章. 2. Maven配置 首先,我们需 ...

  9. chrome charset使用_使用JWT保护你的Spring Boot应用 Spring Security实战

    关键词 Spring Boot.OAuth 2.0.JWT.Spring Security.SSO.UAA 写在前面 最近安静下来,重新学习一些东西,最近一年几乎没写过代码.整天疲于奔命的日子终于结束 ...

最新文章

  1. 解决push的时候有时候会卡一下的问题
  2. CCNP-16 OSPF试验12(BSCI)
  3. GitFlow 工作流和Code Review教程
  4. 2016蓝桥杯省赛---java---B---1(煤球数目)
  5. SpringCloud Gateway 集成 oauth2 实现统一认证授权_03
  6. 2021年中国一次性弹性泵市场趋势报告、技术动态创新及2027年市场预测
  7. 服务器启动jupyter
  8. json数据类型基本转换
  9. 2017第121届中国进出口商品交易会(广交会)-第三期会刊(参展商名录)
  10. linux基础ppt下载,《Linux基础》PPT课件.ppt
  11. Windows_5种方法解除Windows密码
  12. python阿拉伯数字转中文_阿拉伯数字转化为中文数字
  13. 进化树构建的方法原理及检验
  14. 《Machine Learning in Action》—— hao朋友,快来玩啊,决策树呦
  15. UNITER: UNiversal Image-TExt Representation Learning
  16. python--Django快速入门之模板层详解
  17. npm包--淘宝镜像下载
  18. 一步解压缩目录下所有的压缩文件
  19. Google Adsense西联快汇收款流程
  20. Netty内存池 (5w长文+史上最全)

热门文章

  1. Multer的基本使用
  2. python中np.max与np.maximum的区别
  3. 交互设计学习心得体会
  4. HTTP 302跳转(转载)
  5. 基于3DSOM的侧影轮廓方法空间三维模型重建
  6. 从DRA音频标准(国标级)来看技术创新(二)
  7. [文本处理]——Python实现全角字符转化为半角字符
  8. 数说热点|米哈游新作《崩坏:星穹铁道》今日公测,能否再现原神奇迹?
  9. 翻译 CRUSH: Controlled, Scalable,Decentralized Placement of Replicated Data
  10. javaFX环境配置