rest风格的api一般是使用oauth2协议或者是rest + jwt模式,我们这里使用的是后者。
改造过程主要分为以下几步:
需要注意的是,登录操作的模式是不变的。 禁用shiro session
public class StatelessDefaultSubjectFactory extends DefaultWebSubjectFactory { public Subject createSubject(SubjectContext context) { //不创建session context.setSessionCreationEnabled(false); return super.createSubject(context); } }
通过调用context.setSessionCreationEnabled(false)表示不创建会话;如果之后调用Subject.getSession()将抛出DisabledSessionException异常。
/** * 从数据声明生成令牌 * * @param claims 数据声明 * @return 令牌 */ public static String generateToken(Map<String, Object> claims) { Date expirationDate = new Date(System.currentTimeMillis() + EXPIRE_TIME); return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, SECRET).compact(); } /** * 从令牌中获取用户名 * * @param token 令牌 * @return 用户名 */ public static String getUsernameFromToken(String token) { Claims claims = getClaimsFromToken(token); return claims.getSubject(); } /** * 验证令牌 * * @param token * @param username * @return */ private static Boolean validateToken(String token, String username) { String userName = getUsernameFromToken(token); return (userName.equals(username) && !isTokenExpired(token)); } /** * 从令牌中获取数据声明 * * @param token 令牌 * @return 数据声明 */ private static Claims getClaimsFromToken(String token) { Claims claims; try { claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); } catch (Exception e) { claims = null; } return claims; } ...
主要有生成token和校验token的方法。
public class StatelessToken implements AuthenticationToken { private String username; private String token; public StatelessToken(String username, String token) { this.username = username; this.token = token; } @Override public Object getPrincipal() { return username; } @Override public Object getCredentials() { return token; }}
public class StatelessRealm extends AuthorizingRealm { @Override public boolean supports(AuthenticationToken token) { //仅支持StatelessToken类型的Token return token instanceof StatelessToken; } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { //根据用户名查找角色,请根据需求实现 String username = (String) principals.getPrimaryPrincipal(); //TODO 根据用户查找角色,根据自己的业务实现,这里角色硬编码为admin SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); authorizationInfo.addRole("admin"); return authorizationInfo; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { StatelessToken statelessToken = (StatelessToken) token; String username = (String) statelessToken.getPrincipal(); //生成token,这里生成token的方式依实际情况而定,可以定义得复杂些 String generateToken = JwtUtil.generateToken(ImmutableMap.of(username,username)); //然后进行客户端消息摘要和服务器端消息摘要的匹配 return new SimpleAuthenticationInfo( username, generateToken, getName()); }}
关于doGetAuthenticationInfo的返回的SimpleAuthenticationInfo分析如下:
public AuthenticatingRealm() { this(null, new SimpleCredentialsMatcher()); }
使用的是SimpleCredentialsMatcher。
2.父类中调用doGetAuthenticationInfo方法的地方:
这里需要注意以下几点:
3.org.apache.shiro.realm.AuthenticatingRealm#assertCredentialsMatch:
这里使用的是SimpleCredentialsMatcher,我们看下它的doCredentialsMatch方法:
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { Object tokenCredentials = getCredentials(token); Object accountCredentials = getCredentials(info); return equals(tokenCredentials, accountCredentials); }protected Object getCredentials(AuthenticationToken token) { return token.getCredentials(); } protected Object getCredentials(AuthenticationInfo info) { return info.getCredentials(); }
public SimpleAuthenticationInfo(Object principal, Object credentials, String realmName) { this.principals = new SimplePrincipalCollection(principal, realmName); this.credentials = credentials; }
可见,this.credentials为第二个入参。
它的onAccessDenied方法改造如下:
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest req = (HttpServletRequest) request; String jwt = req.getHeader("Authorization"); //可以使用userid String username = req.getHeader("username"); if (JwtUtil.validateToken(jwt)) { StatelessToken statelessToken = new StatelessToken(username, jwt); try { //委托realm进行登录认证 getSubject(request, response).login(statelessToken); return true; }catch (Exception e) { return false; } } redirectToLogin(request,response); return false; }
除了从header中获取外,还可以从cookie中存取。
这里需要注意一个问题,当token被人窃取之后,放入header就能无限制登录了。解决办法主要是像oauth2那样加入refreshToken机制,具体的请自行查看之前关于oauth2的推文。
/** * Shiro的Web过滤器Factory 命名:shiroFilter */ @Bean(name = "shiroFilter") public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); //Shiro的核心安全接口,这个属性是必须的 shiroFilterFactoryBean.setSecurityManager(securityManager); Map<String, Filter> filterMap = new LinkedHashMap<>(); filterMap.put("authc", new AjaxPermissionsAuthorizationFilter()); shiroFilterFactoryBean.setFilters(filterMap); /*定义shiro过滤链 Map结构 * Map中key(xml中是指value值)的第一个'/'代表的路径是相对于HttpServletRequest.getContextPath()的值来的 * anon:它对应的过滤器里面是空的,什么都没做,这里.do和.jsp后面的*表示参数,比方说login.jsp?main这种 * authc:该过滤器下的页面必须验证后才能访问,它是Shiro内置的一个拦截器org.apache.shiro.web.filter.authc.FormAuthenticationFilter */ Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); /* 过滤链定义,从上向下顺序执行,一般将 / ** 放在最为下边:这是一个坑呢,一不小心代码就不好使了; authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问 */ filterChainDefinitionMap.put("/", "anon"); filterChainDefinitionMap.put("/static/**", "anon"); filterChainDefinitionMap.put("/login/auth", "anon"); filterChainDefinitionMap.put("/login/logout", "anon"); filterChainDefinitionMap.put("/error", "anon"); filterChainDefinitionMap.put("/**", "authc"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } @Bean public SubjectFactory subjectFactory(){ SubjectFactory subjectFactory = new StatelessDefaultSubjectFactory(); return subjectFactory; } /** * 不指定名字的话,自动创建一个方法名第一个字母小写的bean */ @Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(userRealm()); securityManager.setSubjectFactory(subjectFactory()); return securityManager; }
对加上了AuthPassport注解的接口进行拦截,对header或cookie中的token进行有效性校验,或者是refreshToken类似的校验,不符合要求的直接拦截掉。
和shiro一起使用时需要注意灵活应用。因为shiroFilter属于filter它的执行顺序在interceptor之前。