【转载】基于shiro与JWT规范实现的单点登录认证服务

  zyw090111

在一些环境中,可能需要把Web应用做成无状态的,即服务器端无状态,就是说服务器端不会存储像会话这种东西,而是每次请求时access_token进行资源访问。如一些REST风格的API,如果不使用OAuth2协议,就可以使用如REST+JWT认证进行访问。JWT(JSON WEB Token):基于散列的消息认证码,使用一个密钥和一个消息作为输入,生成它们的消息摘要。该密钥只有服务端知道。访问时使用该消息摘要进行传播,服务端然后对该消息摘要进行验证。

认证步骤

  • 1、客户端第一次使用用户名密码访问认证服务器,服务器验证用户名和密码,认证成功,使用用户密钥生成JWT并返回
  • 2、之后每次请求客户端带上JWT
  • 3、服务器对JWT进行验证

自定义shiro拦截器 继承FormAuthenticationFilter,并改写核心认证逻辑即可

/**
* Created by LiangT on 2017/5/24.
*/
public class StatelessAuthcFilter extends FormAuthenticationFilter {
private final Logger logger = LoggerFactory.getLogger(StatelessAuthcFilter.class);
@Autowired
private SysUserMapper sysUserMapper;
/**
 * shiro权限拦截核心方法 返回true允许访问resource,这里改写了shiro源码实现,使用JWT进行认证
 *
 * author liangGTY
 * date 2017/5/25
 *
 * @param
 */
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
    //获取token
    String accessToken = getaccessToken((HttpServletRequest) request);
    if (StringUtils.isBlank(accessToken)) {
        return false;
    }
    //获取userId
    Integer userId = getUserIdForToken(accessToken);
    SysUser user = sysUserMapper.selectByPrimaryKey(userId);
    //获取用户的密钥
    String key = JWTUtil.getKey(user);
    try {
        //ExpiredJwtException JWT已过期
        //SignatureException JWT可能被篡改
        Jwts.parser().setSigningKey(key).parseClaimsJws(accessToken).getBody();
    } catch (Exception e) {
        logger.info("authentication fali  --  accessToken : {}", accessToken);
        try {
            onLoginFail(response);
        } catch (IOException e1) {
            logger.error("io exception");
        }
        return false;
    }
    // 将userId放进ThreadLocal中 方便后续业务代码获取
    RequestUtil.put(userId);
    return true;
}
/**
 * 当访问拒绝时是否已经处理了;如果返回true表示需要继续处理;如果返回false表示该拦截器实例已经处理 * 了,将直接返回即可。
 *
 * author liangGTY
 * date 2017/5/25
 *
 * @param
 */
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
    //return super.onAccessDenied(request, response);
    //return true;
    if (isLoginRequest(request, response)) {
        if (isLoginSubmission(request, response)) {
            return executeLogin(request, response);
        } else {
            return true;
        }
    } else {
        //saveRequestAndRedirectToLogin(request, response);
        onLoginFail(response);
        return false;
    }
}
//鉴权失败 返回错误信息
private void onLoginFail(ServletResponse response) throws IOException {
    HttpServletResponse httpServletResponse = (HttpServletResponse) response;
    Map result = new HashMap();
    result.put(Constants.RES_ACCESS_KEY,false);
    result.put(Constants.RES_MSG_KEY,Constants.RES_MSG_NO_ACCESS_VALUE);
    String json = JSON.toJSONString(result);
    httpServletResponse.setHeader("Content-type", "application/json;charset=UTF-8");
    httpServletResponse.getWriter().write(json);
}
/**
 * 解码JWT 从payload中获取userId
 * @param accessToken
 * @return
 */
private Integer getUserIdForToken(String accessToken) {
    Integer userId;
    String payload = JWTUtil.parseBase64UrlEncodedPayload(accessToken);
    Map claims = JSON.parseObject(payload, Map.class);
    Object value = claims.get(Constants.PARAM_USER_ID);
    if (value == null) {
        return null;
    }
    if (value instanceof Integer) {
        userId = (Integer) value;
    } else if (value instanceof String) {
        userId = Integer.valueOf((String) value);
    } else {
        return null;
    }
    return userId;
}
/**
 * 从request从获取token, 先从uri参数中获取 如没有 再从cookie中获取
 * @param request
 * @return
 */
private String getaccessToken(HttpServletRequest request) {
    String accessToken = request.getParameter(Constants.PARAM_DIGEST);
    if (!StringUtils.isBlank(accessToken)) {
        return accessToken;
    }
    Cookie[] cookies = request.getCookies();
    if(null==cookies||cookies.length==0){
        return null;
    }
    for (Cookie cookie : cookies) {
        if (cookie.getName().equals(Constants.PARAM_DIGEST)) {
            accessToken = cookie.getValue();
            continue;
        }
    }
    return accessToken;
}
}

申请access_token实现

/**

 * 功能描述:登陆

 * 

 * author liangGTY

 * date 2017/4/21

 *

 * @param

 */

@RequestMapping("/login.do")

@ResponseBody

public Object login(HttpServletRequest request,HttpServletResponse response, String username, String password) {

    //返回参数

    Map result = new HashMap();

    //验证用户名密码,使用shiro进行匹配 如不喜欢 也可以自己验证

    if (!authentication(username, password, response, result)) {

        result.put(Constants.RES_ACCESS_KEY, Constants.RES_ACCESS_FAIL);

        result.put(Constants.PARAM_DIGEST,null);

        return result;

    }

    //获取令牌

    String token = getToken(username);

    //设置JWT到cookie

    setCookie(response,Constants.PARAM_DIGEST,token);

    //返回参数

    result.put(Constants.RES_MSG_KEY,"success");

    result.put(Constants.RES_ACCESS_KEY, Constants.RES_ACCESS_SUCCESS);

    result.put(Constants.PARAM_DIGEST, token);

    return result;

}

private boolean authentication(String username, String password, HttpServletResponse response, Map result) {

  Subject subject = SecurityUtils.getSubject();

  AuthenticationToken token = new UsernamePasswordToken(username, password);

  try {

      subject.login(token);

  } catch (LockedAccountException e) {

      result.put(Constants.RES_MSG_KEY, "账号已禁用");

      //response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

      return false;

  } catch (UnknownAccountException e) {

      //response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

      result.put(Constants.RES_MSG_KEY, "账号不存在");

      return false;

  } catch (IncorrectCredentialsException e) {

      //response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

      result.put(Constants.RES_MSG_KEY, "密码不正确");

      return false;

  }

  return true;

}

JWT工具类

/**

 * JWTUtil

 *

 * @author liangGTY

 * @date 2017/5/25

 */

public class JWTUtil {

    public static final String SALT = "liangGTY";

    public static final char SEPARATOR_CHAR = '.';

    public static String creatCompact(String secretkey, Map claims) {

        checkNotNull(secretkey,"secrekey not null");

        String compact = Jwts.builder()

                .setClaims(claims)

                .setExpiration(DateUtils.addDays(new Date(), 40)) //令牌有效期,40天

                .signWith(SignatureAlgorithm.HS512, secretkey) //密钥

                .compact();

        return compact;

    }

    public static String parseBase64UrlEncodedPayload(String accessToken) {

        String base64UrlEncodedHeader = null;

        String base64UrlEncodedPayload = null;

        int delimiterCount = 0;

        StringBuilder sb = new StringBuilder(128);

        for (char c : accessToken.toCharArray()) {

            if (c == SEPARATOR_CHAR) {

                CharSequence tokenSeq = Strings.clean(sb);

                String token = tokenSeq != null ? tokenSeq.toString() : null;

                if (delimiterCount == 0) {

                    base64UrlEncodedHeader = token;

                } else if (delimiterCount == 1) {

                    base64UrlEncodedPayload = token;

                }

                delimiterCount++;

                sb.setLength(0);

            } else {

                sb.append(c);

            }

        }

        //base64解码,获取payload

        org.apache.commons.codec.binary.Base64 base64 = new Base64();

        byte[] decode = base64.decode(base64UrlEncodedPayload);

        return new String(decode);

    }

    public static String getKey(SysUser user) {

        // TODO: 密钥算法优化

        return user.getPassword() + SALT + user.getUserName();

    }

    private String getToken(String username) {

        //设置用户ID

        claims.put(Constants.PARAM_USER_ID, getUserId());

        //获取key

        String key = JWTUtil.getKey(user);

        String compact = JWTUtil.creatCompact(key, claims);

        return compact;

    }

}

用于认证的Realm

/**

* SysRealm

*

* @author liangGTY

* @date 2017/4/17

*/

public class SysRealm extends AuthorizingRealm {

@Autowired

private SysUserService sysUserService;

@Override

protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

    SysUser user = RequestUtil.getLogin();

    Integer id = user.getId();

    SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();

    // 获取roles并设置

    Set roles = sysUserService.findUserRolesById(id);

    authorizationInfo.setRoles(roles);

    return authorizationInfo;

}

/**

 * 认证回调函数,登录时调用

 * 首先根据传入的用户名获取User信息;然后如果user为空,那么抛出没找到帐号异常UnknownAccountException;

 * 如果user找到但锁定了抛出锁定异常LockedAccountException;最后生成AuthenticationInfo信息,

 * 交给间接父类AuthenticatingRealm使用CredentialsMatcher进行判断密码是否匹配,

 * 如果不匹配将抛出密码错误异常IncorrectCredentialsException;

 */

@Override

protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

    String username = (String) token.getPrincipal();

    SysUserDO user = sysUserService.findUserByUsername(username);

    if (user == null) {

        throw new UnknownAccountException();

    }

    if(user.getStatus()==1){

        throw new LockedAccountException();

    }

    SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user.getUserName(),

            user.getPassword(),

            getName()

    );

    return authenticationInfo;

}

}

配置文件

<bean id="statelessAuthcFilter" class="com.onway.web.shiro.filter.StatelessAuthcFilter"/>

<bean id="subjectFactory"

    class="com.onway.web.shiro.factory.StatelessDefaultSubjectFactory"/>

<bean id="sessionManager" class="org.apache.shiro.session.mgt.DefaultSessionManager">

  <property name="sessionValidationSchedulerEnabled" value="false"/>

bean>

<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">

  <property name="realm" ref="realm">property>

  <property name="subjectDAO.sessionStorageEvaluator.sessionStorageEnabled"

            value="false"/>

  <property name="subjectFactory" ref="subjectFactory"/>

  <property name="sessionManager" ref="sessionManager"/>

bean>

<bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">

  <property name="staticMethod"

            value="org.apache.shiro.SecurityUtils.setSecurityManager"/>

  <property name="arguments" ref="securityManager"/>

bean>

<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">

  <property name="securityManager" ref="securityManager">property>

  <property name="loginUrl" value="/login.htm">property>

  <property name="successUrl" value="/index.htm">property>

  <property name="unauthorizedUrl" value="/error.jsp">property>

  <property name="filters">

      <map>

          <entry key="statelessAuthc" value-ref="statelessAuthcFilter"/>

      map>

  property>

  <property name="filterChainDefinitions">

      <value>

          /user/login.do = anon

          /jquery-easyui-1.4.5/** = anon

          /font-awesome-4.7.0/** = anon

          /bootstrap-3.3.7-dist/** = anon

          /img/** = anon

          /** = statelessAuthc

      value>

  property>

bean>

写在最后

由于JWT特性,不需要在服务端保存JWT,也不需要在服务端生成session,虽然服务端可能需要做解码与编码的一些计算,但相对于在服务端对session的管理来说,这点性能损耗的是很小的,可以说JWT就适合用来做SSO
这里也可以使用spring-security-oauth2与JWT的配合

原文地址http://dysania.cn/2017/05/26/shiro-JWT/