Shiro之Springboot+shirot+jwt验证-yellowcong

版权声明:本文为博主yellowcong原创文章,未经博主允许不得转载。 https://blog.csdn.net/yelllowcong/article/details/86236775

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

猜你喜欢

转载自blog.csdn.net/yelllowcong/article/details/86236775