Jwt可以做到 前后段分离的操作,我们这个案例中,讲解了如何生成token,以及如何在Header中使用jwt的token,其中没有讲解如何做到token失效,解决方法是,将所有的token存到redis中,如果用户退出,就将redis的token干掉。
1 学习准备
Java之Springboo使用jwt-yellowcong
2 代码地址
https://gitee.com/yellowcong/springboot-demo/tree/master/springboot-shiro-jwt
3 实现效果
我们先从web登陆,生成一个token的结果,然后直接请求服务器的某个地址,不带token,发现访问失败了,当我们设定了token后,可以正常的访问到服务。
4 目录结构
4.1 JwtUtils
通过这个来加密和解密jwt的token
package com.yellowcong.utils;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException;
public class JwtUtils {
//密钥值
private static final String JWK_SOLT = "yellowcong";
//超时时间为 8h
public static final long TIME_LIMIT = 8 * 6000 * 1000 ;
//密钥,base64加密
public static final byte [] SECRET_KEY = getBase64Str(JWK_SOLT);
/**
* 验证 签名和解密的方法
* @param args
*/
public static void main(String[] args) {
//加密token信息
String token = sign("yellowcong");
System.out.println(token);
//获取token里面的数据
Map<String,Object> data = validate(token);
System.out.println("用户名:\t"+data.get("username"));
//日期时间
System.out.println(((Date)data.get("expiration")).toLocaleString());
}
/**
* 添加签名
* @param username 用户的名称
* @return 返回加密后的token 信息
*/
public static String sign(String username) {
//获取到builder
JwtBuilder builder = Jwts.builder()
.setIssuer("yellowcong.com") //签发的人
.setSubject(username) //所接收的人
.setIssuedAt(new Date()) //签发时间
.setExpiration(new Date(System.currentTimeMillis() + TIME_LIMIT)) //过期时间
.signWith(SignatureAlgorithm.HS512, SECRET_KEY); //签名
return builder.compact();
}
/**
* 验证token是否有效,并获取内容
* @param token 需要校验的token
* @return map<>
*/
public static Map<String,Object> validate(String token){
try {
//解析token信息
Claims claims = Jwts.parser()
.setSigningKey(SECRET_KEY) //密钥
.parseClaimsJws(token) //解密的字符串
.getBody();
String username = claims.getSubject();
Map<String,Object> objs = new HashMap<String,Object>(1);
objs.put("username", username);
objs.put("expiration",claims.getExpiration());
return objs;
} catch (Exception e) {
}
return null;
}
/**
* 验证token是否有效,并获取内容
* @param token 需要校验的token
* @return map<>
*/
public static String getUsername(String token){
try {
//解析token信息
Claims claims = Jwts.parser()
.setSigningKey(SECRET_KEY) //密钥
.parseClaimsJws(token) //解密的字符串
.getBody();
//获取订阅者
String username = claims.getSubject();
return username;
} catch (Exception e) {
}
return null;
}
/**
* 获取64编码的字符串
* @param str
* @return
*/
private static byte [] getBase64Str(String str) {
try {
//通过base 64进行编码
return Base64.getEncoder().encode(str.getBytes("utf-8"));
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
4.2 JwtFilter
自定义jwt的过滤器,这样每次带token的都自动登陆服务器了。 在 isAccessAllowed 这个方法中,定义了当访问不允许的,都重定向到一个界面。
package com.yellowcong.filter;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;
import com.yellowcong.token.JwtToken;
public class JwtFilter extends BasicHttpAuthenticationFilter {
/**
* 对跨域提供支持
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
/**
* 判断用户是否想要登入。
* 检测header里面是否包含Authorization字段即可
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
String authorization = req.getHeader("Authorization");
return authorization != null;
}
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
try {
executeLogin(request, response);
return true;
} catch (Exception e) {
this.responseUnAuth(request, response);
return false;
}
}
/**
* 将非法请求跳转到 /401
*/
private void responseUnAuth(ServletRequest req, ServletResponse resp) {
try {
HttpServletResponse httpServletResponse = (HttpServletResponse) resp;
httpServletResponse.sendRedirect("/user/unauthorized");
} catch (Exception e) {
}
}
/**
* 过滤器,每次登陆的时候,都从Header的Authorization 字段中,获取到token,然后执行登陆的操作
*/
@Override
protected boolean executeLogin(ServletRequest request,
ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
//header 需要这个字段
String token = httpServletRequest.getHeader("Authorization");
JwtToken jwtToken = new JwtToken(token);
String url = ((HttpServletRequest)request).getRequestURI();
System.out.println(url);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
getSubject(request, response).login(jwtToken);
// 如果没有抛出异常则代表登入成功,返回true
return true;
}
}
4.3 JwtToken
自定义token,我们需要继承AuthenticationToken 这个抽象类,然后实现getPrincipal 和 getCredentials 两个方法。
package com.yellowcong.token;
import org.apache.shiro.authc.AuthenticationToken;
/**
* 创建token
* @author yellowcong
*
*/
public class JwtToken implements AuthenticationToken{
//jwt的token密钥
private String token ;
public String getToken() {
return token;
}
public JwtToken(String token) {
super();
this.token = token;
}
@Override
public Object getCredentials() {
// TODO Auto-generated method stub
return token;
}
@Override
public Object getPrincipal() {
// TODO Auto-generated method stub
return token;
}
}
4.4 AuthorizingRealm
创建jwt的验证器,一定要加上 public boolean supports(AuthenticationToken token)
这个方法 ,不然,就会导致找不到Realm来处理这个Token服务
package com.yellowcong.realm;
import java.util.HashSet;
import java.util.Set;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.apache.shiro.authc.AuthenticationToken;
import com.yellowcong.model.User;
import com.yellowcong.service.UserService;
import com.yellowcong.token.JwtToken;
import com.yellowcong.utils.JwtUtils;
/**
* 创建日期:2017年9月23日 <br/>
* 创建用户:yellowcong <br/>
* 功能描述:用于授权操作
*/
public class JwtRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
/**
* 判断token是否事我们的这个jwttoekn
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* 用户授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection paramPrincipalCollection) {
//获取jwt的用户名和密码信息
String username = JwtUtils.getUsername(paramPrincipalCollection.toString());
System.out.println(username);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 根据用户ID查询角色(role),放入到Authorization里。
Set<String> roles = new HashSet<String>(); // 添加用户角色
roles.add("administrator");
info.setRoles(roles);
// 根据用户ID查询权限(permission),放入到Authorization里。
Set<String> permissions = new HashSet<String>(); // 添加权限
permissions.add("/role/**");
info.setStringPermissions(permissions);
return info;
}
/**
* 认证,用户登录
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken paramAuthenticationToken) throws AuthenticationException {
JwtToken jwtToken = (JwtToken) paramAuthenticationToken;
String token = jwtToken.getToken();
//获取token 信息
String username = JwtUtils.getUsername(token);
if(username == null || "".equals(username.trim())) {
throw new AuthenticationException("用户名为空");
}
//判断用户 是否存在的情况
User user = this.userService.getUserByName(username);
if(user == null) {
throw new AuthenticationException("用户不存在");
}
//user信息
return new SimpleAuthenticationInfo(token, token, getName());
}
}
4.5 ShiroConfig
配置Jwt的filter以及过滤方式到配置中
package com.yellowcong.config;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.servlet.Filter;
import org.apache.shiro.cache.MemoryConstrainedCacheManager;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.web.filter.DelegatingFilterProxy;
import com.yellowcong.filter.JwtFilter;
import com.yellowcong.realm.JwtRealm;
@Configuration
@PropertySource("classpath:shiro.properties") // 指定读取的配置文件地址
public class ShiroConfig {
@Bean
public FilterRegistrationBean filterRegistrationBean() {
FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
filterRegistration.setFilter(new DelegatingFilterProxy("shiroFilter"));
filterRegistration.addInitParameter("targetFilterLifecycle", "true");
filterRegistration.setEnabled(true);
filterRegistration.addUrlPatterns("/*");
return filterRegistration;
}
@Bean(name = "lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator daap = new DefaultAdvisorAutoProxyCreator();
daap.setProxyTargetClass(true);
return daap;
}
@Bean
public JwtRealm getSampleRealm(){
return new JwtRealm();
}
@Bean(name = "securityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//设定realm 用于权限的认证
securityManager.setRealm(this.getSampleRealm());
//用于securityManager 的缓存
securityManager.setCacheManager(new MemoryConstrainedCacheManager());
//关闭shiro自带的session
//参考文档
//http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator evaluator = new DefaultSessionStorageEvaluator();
evaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(evaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
/**
* shiro的过滤器
* @param securityManager
* @param casFilter
* @param casServerUrlPrefix
* @param shiroServerUrlPrefix
* @return
*/
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager,
@Value("${shiro.loginUrl}") String loginUrl, //登陆地址
@Value("${shiro.successUrl}") String successUrl, //登陆成功地址
@Value("${shiro.unauthorizedUrl}") String unauthorizedUrl) { //未授权的地址
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//设定认证管理的Manager
shiroFilterFactoryBean.setSecurityManager(securityManager);
//设定登陆地址
shiroFilterFactoryBean.setLoginUrl(loginUrl);
//设定登陆成功跳转的地址
shiroFilterFactoryBean.setSuccessUrl(successUrl);
//设定未授权的地址
shiroFilterFactoryBean.setUnauthorizedUrl(unauthorizedUrl);
// 添加自己的过滤器并且取名为jwt
Map<String, Filter> filterMap = new HashMap<>(1);
filterMap.put("jwt", new JwtFilter());
shiroFilterFactoryBean.setFilters(filterMap);
//添加授权
Map<String,String> chainMap = new LinkedHashMap<>();
chainMap.put("/user/login", "anon"); //登陆界面任何人可以访问
chainMap.put("/error", "anon"); // erro的信息
chainMap.put("/user/unauthorized", "anon"); // erro的信息
chainMap.put("/resources/img/**", "anon"); //图片样式可以直接访问
chainMap.put("/resources/js/**", "anon"); //js可以直接访问
chainMap.put("/favicon.ico", "anon"); //js可以直接访问
chainMap.put("/user/list", "jwt"); //用户列表需要授权
//设定授权方式为jwt
chainMap.put("/**", "jwt"); // 余下的页面,全都需要进行授权操作
shiroFilterFactoryBean.setFilterChainDefinitionMap(chainMap);
return shiroFilterFactoryBean;
}
}
4.6 用户登陆处理
jwt的用户登陆处理,简单来讲,就是生成一个token给客户端(这个客户端,指的是前后端分离的web界面)使用。对于token过期,这个操作,可以将生成的ticket丢到redis中,如果退出,直接将redis里面也删掉, 这样就解决了jwt退出的问题了。
package com.yellowcong.controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
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.ResponseBody;
import com.yellowcong.service.UserService;
import com.yellowcong.token.JwtToken;
import com.yellowcong.utils.JwtUtils;
/**
* 创建日期:2018年5月8日<br/>
* 代码创建:黄聪<br/>
* 功能描述:<br/>
*/
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
UserService userService;
/**
* 用户列表
* @param map
* @return
*/
/*@ResponseBody
@RequestMapping("/list")
public String list(ModelMap map){
Subject subject = SecurityUtils.getSubject();
String token = subject.getPrincipal().toString();
String user = JwtUtils.getUsername(token);
return "欢迎用户"+user;
}*/
// @ResponseBody
@RequestMapping("/list")
public String list(ModelMap map){
Subject subject = SecurityUtils.getSubject();
String token = subject.getPrincipal().toString();
String user = JwtUtils.getUsername(token);
map.put("username", user);
return "/user/list";
}
@ResponseBody
@RequestMapping("/unauthorized")
public String unauthorized(){
return "用户未授权";
}
@ResponseBody
@RequestMapping("/page2")
public String testPage2(){
return "测试需要授权的页面";
}
/**
* 跳转到登录页面
* 创建日期:2017年9月23日<br/>
* 创建用户:yellowcong<br/>
* 功能描述:
* @return
*/
@RequestMapping(value="/login",method=RequestMethod.GET)
public String loginInput(){
return "user/loginInput";
}
/**
* 退出操作
* @return
*/
@RequestMapping(value="/logout")
public String logout(){
//登出系统
SecurityUtils.getSubject().logout();
return "user/loginInput";
}
/**
* 创建日期:2017年9月23日<br/>
* 创建用户:yellowcong<br/>
* 功能描述:用户登录操作
* @param username
* @param password
* @return
*/
@ResponseBody
@RequestMapping(value="/login",method=RequestMethod.POST)
public String login(String username,String password){
try {
//获取subject信息
Subject subject = SecurityUtils.getSubject();
//判断用户是否授权,解决已经登陆授权的用户,重新登陆授权
if(!this.userService.chkUser(username, password)) {
throw new RuntimeException("用户登陆失败");
}
//检查用户信息
String tokenStr = JwtUtils.sign(username);
//用户登录
JwtToken token = new JwtToken(tokenStr);
//登录用户
subject.login(token);
return token.getToken();
} catch (Exception e) {
e.printStackTrace();
}
return "登陆失败";
}
@ResponseBody
@RequestMapping("/error")
public String error(){
return "认证失败";
}
}
常见问题
1. Please ensure that the appropriate Realm implementation is configured correctly or that the realm accepts AuthenticationTokens of this type.
导致这个问题的原因是 不知道使用哪个Ream导致的,这个问题是由于我自定义了Reaml后,没有覆写supports 这个方法导致的。
org.apache.shiro.authc.pam.UnsupportedTokenException: Realm [com.yellowcong.realm.JwtRealm@3bbcab03] does not support authentication token [com.yellowcong.token.JwtToken@743b74cb]. Please ensure that the appropriate Realm implementation is configured correctly or that the realm accepts AuthenticationTokens of this type.
at org.apache.shiro.authc.pam.ModularRealmAuthenticator.doSingleRealmAuthentication(ModularRealmAuthenticator.java:178)
at org.apache.shiro.authc.pam.ModularRealmAuthenticator.doAuthenticate(ModularRealmAuthenticator.java:267)
at org.apache.shiro.authc.AbstractAuthenticator.authenticate(AbstractAuthenticator.java:198)
2 . 服务跳转失败
导致这个问题的原因是,由于有些跳转可能需要jwt授权,我们开发的时候不知道,然后导致打死都跳转不过去了,解决这个问题的方式是,在JwtFilter 这个类中,打印请求的地址,然后排查,哪些地址是需要token的,哪些是不需要token的。
String url = ((HttpServletRequest)request).getRequestURI();
System.out.println(url);
参考文章
https://www.tuicool.com/articles/AfU3QjN
https://www.jianshu.com/p/f37f8c295057
https://412887952-qq-com.iteye.com/blog/2359098