使用JWT和Spring Security保护REST API
通常情况下,把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的工作流程
- 用户导航到登录页,输入用户名、密码,进行登录
- 服务器验证登录鉴权,如果改用户合法,根据用户的信息和服务器的规则生成JWT Token
- 服务器将该token以json形式返回(不一定要json形式,这里说的是一种常见的做法)
- 用户得到token,存在localStorage、cookie或其它数据存储形式中。
- 以后用户请求/protected中的API时,在请求的header中加入 Authorization: Bearer xxxx(token)。此处注意token之前有一个7字符长度的 Bearer
- 服务器端对此token进行检验,如果合法就解析其中内容,根据其拥有的权限和自己的业务逻辑给出对应的响应结果。
- 流程图如下:
为了更好的理解这个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相关推荐
- tomcat使用ssl_使用SSL和Spring Security保护Tomcat应用程序的安全
tomcat使用ssl 如果您看过我的上一个博客,您会知道我列出了Spring Security可以做的十件事 . 但是,在开始认真使用Spring Security之前,您真正要做的第一件事就是确保 ...
- 使用Spring Security保护REST服务
总览 最近,我正在一个使用REST服务层与客户端应用程序(GWT应用程序)进行通信的项目中. 因此,我花了很多时间来弄清楚如何使用Spring Security保护REST服务. 本文介绍了我找到的解 ...
- 使用SSL和Spring Security保护Tomcat应用程序的安全
如果您看过我的上一个博客,您会知道我列出了Spring Security可以做的十件事 . 但是,在认真开始使用Spring Security之前,您真正要做的第一件事就是确保您的Web应用使用正确的 ...
- 使用带有OAuth的Spring Security保护资源
1.简介 在本教程中,我们将研究如何使用Spring Security和OAuth来基于路径模式( / api / ** )保护服务器上的管理资源. 我们配置的另一个路径模式( / oauth / t ...
- gwt格式_使用Spring Security保护GWT应用程序的安全
gwt格式 在本教程中,我们将看到如何将GWT与Spring的安全模块(即Spring Security)集成. 我们将看到如何保护GWT入口点,如何检索用户的凭据以及如何记录各种身份验证事件. 此外 ...
- 使用Spring Security保护GWT应用程序
在本教程中,我们将看到如何将GWT与Spring的安全模块(即Spring Security)集成在一起. 我们将看到如何保护GWT入口点,如何检索用户的凭据以及如何记录各种身份验证事件. 此外,我们 ...
- JWT实战 Spring Security Oauth2整合JWT 整合SSO单点登录
文章目录 一.JWT 1.1 什么是JWT 1.2 JWT组成 头部(header) 载荷(payload) 签名(signature) 如何应用 1.3 JJWT 快速开始 创建token toke ...
- 将JWT与Spring Security OAuth结合使用
1.概述 在本教程中,我们将讨论如何使用Spring Security OAuth2实现来使用JSON Web令牌. 我们还将继续构建此OAuth系列的上一篇文章. 2. Maven配置 首先,我们需 ...
- chrome charset使用_使用JWT保护你的Spring Boot应用 Spring Security实战
关键词 Spring Boot.OAuth 2.0.JWT.Spring Security.SSO.UAA 写在前面 最近安静下来,重新学习一些东西,最近一年几乎没写过代码.整天疲于奔命的日子终于结束 ...
最新文章
- 解决push的时候有时候会卡一下的问题
- CCNP-16 OSPF试验12(BSCI)
- GitFlow 工作流和Code Review教程
- 2016蓝桥杯省赛---java---B---1(煤球数目)
- SpringCloud Gateway 集成 oauth2 实现统一认证授权_03
- 2021年中国一次性弹性泵市场趋势报告、技术动态创新及2027年市场预测
- 服务器启动jupyter
- json数据类型基本转换
- 2017第121届中国进出口商品交易会(广交会)-第三期会刊(参展商名录)
- linux基础ppt下载,《Linux基础》PPT课件.ppt
- Windows_5种方法解除Windows密码
- python阿拉伯数字转中文_阿拉伯数字转化为中文数字
- 进化树构建的方法原理及检验
- 《Machine Learning in Action》—— hao朋友,快来玩啊,决策树呦
- UNITER: UNiversal Image-TExt Representation Learning
- python--Django快速入门之模板层详解
- npm包--淘宝镜像下载
- 一步解压缩目录下所有的压缩文件
- Google Adsense西联快汇收款流程
- Netty内存池 (5w长文+史上最全)
热门文章
- Multer的基本使用
- python中np.max与np.maximum的区别
- 交互设计学习心得体会
- HTTP 302跳转(转载)
- 基于3DSOM的侧影轮廓方法空间三维模型重建
- 从DRA音频标准(国标级)来看技术创新(二)
- [文本处理]——Python实现全角字符转化为半角字符
- 数说热点|米哈游新作《崩坏:星穹铁道》今日公测,能否再现原神奇迹?
- 翻译 CRUSH: Controlled, Scalable,Decentralized Placement of Replicated Data
- javaFX环境配置