JWT是JSON WEB TOKEN的缩写,它是基于 RFC 7519标准定义的一种可以安全传输的的JSON对象,由于使用了数字签名,所以是可信任和安全的。
1.JWT的组成
{"alg": "HS512"}
{"sub":"admin","created":1489079981393,"exp":1489684781}
//secret为加密算法的密钥 String signature = HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
2.JWT实现认证和授权的原理
- IDEA:2018.2(lombok插件)
- SpringBoot:2.3.1.RELEASE
- Shiro:1.4.1
- JWT:3.2.0
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.1</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
</dependencies>
@Slf4j
public class JwtUtil {
private static final Long EXPIRE_TIME = 5 * 60 * 1000L;
private static final String SECRET = "SHIRO+JWT";
/**
* 生成 token 时,指定 token 过期时间 EXPIRE_TIME 和签名密钥 SECRET,
* 然后将 expireDate 和 username 写入 token 中,并使用带有密钥的 HS256 签名算法进行签名
* @param username
* @return
*/
public static String createToken(String username) {
String token = null;
try {
// 过期时间
Date expireDate = new Date(System.currentTimeMillis() + EXPIRE_TIME);
// 加密算法
Algorithm algorithm = Algorithm.HMAC256(SECRET);
token = JWT.create()
.withClaim("username", username)
.withExpiresAt(expireDate)
.sign(algorithm);
} catch (UnsupportedEncodingException e) {
log.error("Failed to create token. {}", e.getMessage());
}
return token;
}
/**
* 验证token,如果验证失败,便会抛出异常
* @param token
* @param username
* @return
*/
public static boolean verify(String token, String username) {
boolean isSuccess = false;
try {
Algorithm algorithm = Algorithm.HMAC256(SECRET);
JWTVerifier verifier = JWT.require(algorithm)
.withClaim("username", username)
.build();
// 验证token
verifier.verify(token);
isSuccess = true;
} catch (UnsupportedEncodingException e) {
log.error("Token is invalid. {}", e.getMessage());
}
return isSuccess;
}
/**
* 在 createToken()方法里,有将 username 写入 token 中。现在可从 token 里获取 username
* @param token
* @return
*/
public static String getUsernameFromToken(String token) {
try {
DecodedJWT decode = JWT.decode(token);
String username = decode.getClaim("username").asString();
return username;
} catch (JWTDecodeException e) {
log.error("Failed to Decode jwt. {}", e.getMessage());
return null;
}
}
}
@Data
public class Account {
private Integer id;
private String username;
private String password;
private String perms;
private String role;
}
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountMapper accountMapper;
@Override
public Account findByUsername(String username) {
QueryWrapper wrapper = new QueryWrapper();
wrapper.eq("username",username);
return accountMapper.selectOne(wrapper);
}
}
/**
* 因为要整合了 JWT ,我们需要自定义过滤器 JWTFilter。
* JWTFilter 继承了 BasicHttpAuthenticationFilter,并部分原方法进行了重写。
*
* 该过滤器主要有三步:
* 1.检验请求头是否带有 Token: ((HttpServletRequest) request).getHeader(“Token”)
* 2.如果带有 Token ,则执行 Shiro 中的 login() 方法,该方法将导致:
* 将 Token 提交到 Realm 中进行验证(执行自定义的Reaml中的方法);
* 如果没有 Token,则说明当前状态为游客状态或者其他一些不需要进行认证的接口
* 3.如果在 Token 校验的过程中出现错误,如:Token 校验失败,
* 那么我会将该请求视为认证不通过,则重定向到 /unauthorized/**
* @author Sakura
*/
@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter {
/**
* 如果请求头带有token,则对token进行检查,否则直接放行
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
//判断请求头是否带有token
if (isLoginAttempt(request,response)){
//如果存在token,则进入executeLogin()方法执行登入,并检测 token 的正确性
try {
executeLogin(request,response);
} catch (Exception e) {
log.error("Error! {}", e.getMessage());
responseError(response, e.getMessage());
}
}
// 如果不存在 token ,则可能是执行登录操作/游客访问状态,所以直接放行
return true;
}
/**
* 检测 header 中是否包含token
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
return getTokenFromRequest(request) !=null;
}
/**
* 执行登录操作
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception{
String token = getTokenFromRequest(request);
JwtToken jwtToken = new JwtToken(token);
// 提交给 realm 进行登录,如果错误,会抛出异常
getSubject(request,response).login(jwtToken);
// 如果没有抛出异常,则代表登录成功,返回true
return true;
}
/**
* 从请求中获取token
* @param request
* @return
*/
private String getTokenFromRequest(ServletRequest request) {
HttpServletRequest req = (HttpServletRequest) request;
return req.getHeader("Token");
}
/**
* 非法请求将跳转到 "/unauthorized/**"
*/
private void responseError(ServletResponse response, String message) {
try {
HttpServletResponse resp = (HttpServletResponse) response;
// 设置编码,否则中文字符在重定向时会变为空字符串
message = URLEncoder.encode(message, "UTF-8");
resp.sendRedirect("/unauthorized/" + message);
} catch (UnsupportedEncodingException e) {
log.error("Error! {}", e.getMessage());
} catch (IOException e) {
log.error("Error! {}", e.getMessage());
}
}
}
/**
* 这里我们自定义了一个AuthenticationToken----JwtToken。
* 因为在Reaml认证方法中,我们是对 Token进行认证的。至于 UsernamePasswordToken (Shiro 中自带),
* 我们需要 对 username 和 password 认证时就可以用它
* @author Sakura
*/
public class JwtToken implements AuthenticationToken {
private String token;
public JwtToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
@Slf4j
public class AccountRealm extends AuthorizingRealm {
@Autowired
private AccountService accountService;
@Override
public boolean supports(AuthenticationToken token){
return token instanceof JwtToken;
}
/**
* 授权
* 从 Token 中获取 username ,然后根据 username 可获取用户信息(角色、权限等)并添加到 AuthorizationInfo 中。
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
log.info("用户授权中");
//获取当前登录的用户信息
String username = JwtUtil.getUsernameFromToken(principalCollection.toString());
Account account = accountService.findByUsername(username);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//设置权限
info.addStringPermission(account.getPerms());
info.addRole(account.getRole());
return info;
}
/**
* 认证
* 拿到从 executeLogin() 方法中传过来的 Token,并对 Token 检验是否有效、用户是否存在以及是否封号
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
log.info("身份认证");
//这里的token是从JwtFilter的executeLogin()方法传递过来的
String token = (String) authenticationToken.getCredentials();
//解密
String username = JwtUtil.getUsernameFromToken(token);
//从数据库汇总获取对应的用户名和面
Account account = accountService.findByUsername(username);
if (StringUtils.isEmpty(username) || !JwtUtil.verify(token,username)){
log.error("token 认证失败");
throw new AuthenticationException("token 认证失败");
}
if (null == account){
log.error("账号或密码错误");
throw new AuthenticationException("账号或密码错误");
}
log.info("用户{}认证成功!", account.getUsername());
return new SimpleAuthenticationInfo(token, token, getName());
}
}
@Configuration
public class ShiroConfig {
@Bean
public AccountRealm accountRealm(){
return new AccountRealm();
}
@Bean
public DefaultWebSecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(accountRealm());
//关闭shiro自带的session
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator evaluator = new DefaultSessionStorageEvaluator();
evaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(evaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
@Bean
public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//设置自定义的拦截器
Map<String, Filter> filterMap = new LinkedHashMap<>();
filterMap.put("jwt",new JwtFilter());
shiroFilterFactoryBean.setFilters(filterMap);
Map<String,String> filterRuleMap = new HashedMap(16);
//设置所有的请求经过自定义的filter
filterRuleMap.put("/**","jwt");
filterRuleMap.put("/unauthorized/**","anon");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterRuleMap);
return shiroFilterFactoryBean;
}
/** 对Shiro注解的支持*/
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
creator.setProxyTargetClass(true);
return creator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
}
在上述配置中,由于我们开启了对Shiro注解的支持,因此可以通过注解的方式来限制相关用户使用某些接口的方法,即给用户授权
@RequiresRoles("admin")
@GetMapping("/enter")
public JwtResultMap enter() {
return resultMap.success().code(200).message("Admin Enter");
}
例如上述代码中的注解@RequiresRoles("admin")
即说明需要有admin的角色才可以访问,而授权操作在AccountRealm中doGetAuthorizationInfo()
方法中有设置相关的权限。
// 拥有 user 或 admin 角色,且拥有 vip 权限可以访问
@RequiresRoles(logical = Logical.OR, value = {"user", "admin"})
@RequiresPermissions("vip")
@RestControllerAdvice
public class JwtException {
@Autowired
private JwtResultMap resultMap;
/**捕获与Shiro相关的异常*/
@ExceptionHandler(ShiroException.class)
public JwtResultMap handle401(){
return resultMap.fail().code(401).message("您没有权限访问!");
}
/**捕获其他异常*/
@ExceptionHandler(Exception.class)
public JwtResultMap globalException(HttpServletRequest request,Throwable e){
return resultMap
.fail()
.code(getStatus(request).value())
.message("访问出错,无法访问:"+e.getMessage());
}
private HttpStatus getStatus(HttpServletRequest request) {
Integer statusCode = (Integer) request.getAttribute("java.servlet.error.status_code");
if (null == statusCode){
return HttpStatus.INTERNAL_SERVER_ERROR;
}
return HttpStatus.valueOf(statusCode);
}
}
@Component
public class JwtResultMap extends HashMap<String,Object> {
public JwtResultMap success(){
this.put("result","success");
return this;
}
public JwtResultMap fail() {
this.put("result", "fail");
return this;
}
public JwtResultMap code(int code) {
this.put("code", code);
return this;
}
public JwtResultMap message(Object message) {
this.put("message", message);
return this;
}
}
GuestController 游客接口
@RestController
@RequestMapping("/guest")
public class GuestController {
@Autowired
private JwtResultMap resultMap;
@GetMapping("/enter")
public JwtResultMap enter() {
return resultMap.success().code(200).message("欢迎进入游客页面" );
}
}
AdminController
@RestController
@RequestMapping("/admin")
public class AdminController {
@Autowired
private JwtResultMap resultMap;
@RequiresRoles("admin")
@GetMapping("/enter")
public JwtResultMap enter(){
return resultMap.success().code(200).message("Admin Enter");
}
}
UserController
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private JwtResultMap resultMap;
@RequiresRoles(logical = Logical.OR,value = {"user","admin"})
@GetMapping("/enter")
public JwtResultMap enter(){
return resultMap.success().code(200).message("欢迎进入用户页面");
}
@RequiresPermissions("vip")
@RequiresRoles(logical = Logical.OR,value = {"user","admin"})
@GetMapping("/getMessage")
public JwtResultMap getMessage(){
return resultMap.success().code(200).message("成功获得vip信息");
}
}
LoginController 登录接口
@Controller
public class LoginController {
@Autowired
private JwtResultMap resultMap;
@Autowired
private AccountService accountService;
@PostMapping("/login")
@ResponseBody
public JwtResultMap login(String username,String password){
Account account = accountService.findByUsername(username);
if (null==account){
return resultMap.fail().code(401).message("账号错误");
}else if (!password.equals(account.getPassword())){
return resultMap.fail().code(401).message("密码错误");
}
return resultMap.success().code(200).message(JwtUtil.createToken(username));
}
@ResponseBody
@GetMapping("/unauthorized/{message}")
public JwtResultMap unauthorized(@PathVariable String message){
return resultMap.success().code(401).message(message);
}
}
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1OTY1OTc1NjEsInVzZXJuYW1lIjoienMifQ.jeac86Kar2R44hJcrjsjBvbSotKHYquZnmQHiWcD8WU
- 接着把刚才获取的Token值复制下来再重新访问用户接口,结果显示可以访问
- 但如果用该token访问管理员接口,会显示权限不够,因为需要admin的角色才可访问
以上便是关于SpringBoot + Shiro + JWT 实现认证和授权