JWT的应用场景
关于JWT是什么,可理解为使用带签名的token来做用户和权限验证,现在流行的公共开放接口用的OAuth 2.0协议基本也是类似的套路。这里只是说下选择使用jwt不用session的原因。 首先,是要支持多端,一个api要支持H5, PC和APP三个前端,如果使用session的话对app不是很友好,而且session有跨域攻击的问题。
Shiro
3个概念
- SecurityManager,可以理解成控制中心,所有请求最终基本上都通过它来代理转发,一般我们程序中不需要直接跟他打交道。
- Subject ,请求主体。比如登录用户,比如一个被授权的app。在程序中任何地方都可以通过SecurityUtils.getSubject()获取到当前的subject。subject中可以获取到Principal,这个是subject的标识,比如登陆用户的用户名或者id等,shiro不对值做限制。但是在登录和授权过程中,程序需要通过principal来识别唯一的用户。
- Realm,这个实在不知道怎么翻译合适。通俗一点理解就是realm可以访问安全相关数据,提供统一的数据封装来给上层做数据校验。shiro的建议是每种数据源定义一个realm,比如用户数据存在数据库可以使用JdbcRealm;存在属性配置文件可以使用PropertiesRealm。一般我们使用shiro都使用自定义的realm。 当有多个realm存在的时候,shiro在做用户校验的时候会按照定义的策略来决定认证是否通过,shiro提供的可选策略有一个成功或者所有都成功等。 一个realm对应了一个CredentialsMatcher,用来做用户提交认证信息和realm获取得用户信息做比对,shiro已经提供了常用的比如用户密码和存储的Hash后的密码的对比。
需求
搭建一个基于Shiro+JWT可以进行用户认证和授权的后台服务
编码
JWT
第一步:编写一个JWT工具类,这个类负责token的加密,解密,是否过期等问题。
public class JWTUtil {
// 过期时间 24 小时
private static final long EXPIRE_TIME = 60 * 24 * 60 * 1000;
/**
* 生成 token
*/
public static String createToken(String username,String password) {
try {
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(password);
// 附带username信息
return JWT.create()
.withClaim("username", username)
//到期时间
.withExpiresAt(date)
//创建一个新的JWT,并使用给定的算法进行标记
.sign(algorithm);
} catch (UnsupportedEncodingException e) {
return null;
}
}
/**
* 校验 token 是否正确
*/
public static boolean verify(String token, String username,String password) {
try {
Algorithm algorithm = Algorithm.HMAC256(password);
//在token中附带了username信息
JWTVerifier verifier = JWT.require(algorithm)
.withClaim("username", username)
.build();
//验证 token
verifier.verify(token);
return true;
} catch (Exception exception) {
return false;
}
}
/**
* 获得token中的信息,无需secret解密也能获得
*/
public static String getUsername(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) {
return null;
}
}
}
第二步:编写Filter,控制是否登入,跨域请求开启的功能
public class JWTFilter extends BasicHttpAuthenticationFilter {
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if(isLoginAttempt(request, response)) {
try {
executeLogin(request, response);
return true;
} catch (Exception e) {
responseError(response, e.getMessage());
}
}
return true;
}
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest requests=(HttpServletRequest) request;
String token = requests.getHeader("Authorization");
return token!=null;
}
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader("Authorization");
JWTToken jwtToken = new JWTToken(token);
// 提交给realm进行登入,如果错误它会抛出异常并被捕获
getSubject(request, response).login(jwtToken);
return true;
}
@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);
}
private void responseError(ServletResponse response, String message) {
HttpServletResponse responses=(HttpServletResponse) response;
message=URLEncoder.encode(message);
try {
responses.sendRedirect("/unauthorized/" + message);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
第三步:重新包装token
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;
}
}
Shiro
第一步:编写Shiro的配置类,定义jwt过滤器,访问策略,securitymanager,realm等。
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager manager) {
ShiroFilterFactoryBean shiroFilterFactoryBean=new ShiroFilterFactoryBean();
Map<String, Filter> filterMap = new LinkedHashMap<>();
//设置我们自定义的JWT过滤器
filterMap.put("jwt", new JWTFilter());
shiroFilterFactoryBean.setFilters(filterMap);
shiroFilterFactoryBean.setSecurityManager(manager);
shiroFilterFactoryBean.setLoginUrl("/login");
shiroFilterFactoryBean.setUnauthorizedUrl("/notRule");
Map<String,String> filterChainDefinitionMap=new LinkedHashMap<>();
filterChainDefinitionMap.put("/**","jwt");
filterChainDefinitionMap.put("/login","anon");
filterChainDefinitionMap.put("/","anon");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean("securityManager")
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置realm.
securityManager.setRealm(customRealm());
/*
* 关闭shiro自带的session
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
@Bean
public CustomRealm customRealm() {
return new CustomRealm();
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
// 强制使用cglib,防止重复代理和可能引起代理出错的问题
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
}
第二步:编写Realm实现认证和授权
public class CustomRealm extends AuthorizingRealm{
@Autowired
UserRepository rp;
/**
* 授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = JWTUtil.getUsername(principals.toString());
SimpleAuthorizationInfo sa=new SimpleAuthorizationInfo();
String role=rp.findByusername(username).getRole();
HashSet<String> set = new HashSet<String>();
set.add(role);
sa.setRoles(set);
return sa;
}
/**
* 认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String token = (String) authenticationToken.getCredentials();
// 解密获得username,用于和数据库进行对比
String username = JWTUtil.getUsername(token);
String password = rp.findByusername(username).getPassword();
if (username == null || !JWTUtil.verify(token, username,password)) {
throw new AuthenticationException("token认证失败!");
}
if (null == password) {
throw new AccountException("用户名不正确");
} else if (!password.equals(new String((char[]) authenticationToken.getCredentials()))) {
throw new AccountException("密码不正确");
}
SimpleAuthenticationInfo simpleinfo=new SimpleAuthenticationInfo(token, token,"MyRealm");
return simpleinfo;
}
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JWTToken;
}
}
总结
在ShiroConfig中配置JWTFilter,SecurityManager[Realm],访问策略。JWTFilter拦截请求并判断是否合法,若合法调用传递给Realm进行认证和授权。 PS:JWT的token其实是通过password对username进行加密签名形成的一串字符串代码。在解析的时候也需要查询数据库获得密码进行解密