The background,

Shiro was used in the taobao customer project last time, and made a simple single sign-on system. At the same time, the separation of front and back ends is a necessary and ongoing path for the company, so the company also has the idea of unified sign-on. Now, the company is in a state of half side before and after the separation, just the code between separated, release or front rely on back-end container, so there is no real separation before and after the end, the company take advantage this completely renovated, so the sso project launched, back-end design falls into my body, at the same time looking for a professional front end with me.

Ii. Requirements in the project

1. After system A logs in, system B will not log in again
2. System A and SYSTEM B may be under different domain names
3, route jump is completely handed over to the front end, the back end does not control
4, support distributed architecture, when the user surge, elastic expansion, to achieve diversion
5. Each system user has his or her own role and authority, and must ensure that no authority cannot access data
6. Two path styles, such as restful and traditional request styles, must be supported
7. Some paths do not require serious permissions, such as open interfaces.

Three, technology selection

Based on the company’s requirements, I chose Redis + Shiro to do distributed support and permission verification, after all, I have Shiro experience and Shiro is a good security framework.

4. Login process

1. Log in

User A logs in → requests the menu permission under the domain name → requests data

2. Jump

User A has logged in and clicks to go to system B → Request the user’s menu permission under the domain name → Front-end rendering menu User A has logged in and enters the URL to jump to System B → Cookie for Token → Request the user’s menu permission under the domain name → Front-end rendering menu note: Token is necessary when the back-end does permission verification. Therefore, switching between multiple systems is actually a token transfer problem.

Five, the code

1. Spring configuration code

<? xml version="1.0" encoding="UTF-8"? > <beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:util="http://www.springframework.org/schema/util"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
       default-autowire="byName"> <! -- Custom Realm --> <bean id="upmsRealm" class="com.ruhnn.controller.shiro.UpmsRealm">
        <property name="credentialsMatcher" ref="credentialsMatcher"/>
        <property name="PermissionResolver" ref="urlPermissionResolver"/> </bean> <! -- credential matcher --> <bean id="credentialsMatcher" class="com.xxx.controller.shiro.CustomCredentialsMatcher"/>

    <bean id="urlPermissionResolver" class="com.xxx.controller.shiro.UrlPermissionResolver"/ > <! -- Security manager --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="upmsRealm"/>
        <property name="sessionManager" ref="sessionManager"/> </bean> <! -- Session Manager --> <! --<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">-->
    <bean id="sessionManager" class="com.ruhnn.controller.shiro.SsoSessionManager"> <! -- Set the global session timeout, default 30 minutes (1800000) --> <property name="globalSessionTimeout" value="1800000"/ > <! -- Whether SessionDAO's delete method will be called to delete the session default after the session expirestrue -->
        <property name="deleteInvalidSessions" value="true"/ > <! -- Session validator scheduling time --> <property name="sessionValidationInterval" value="1800000"/ > <! -- Session storage implementation --> <property name="sessionDAO" ref="redisCacheSessionDAO"/>
        <property name="sessionIdCookie.name" value="SSO-SESSIONID"/> </bean> <! --</bean>--> <! -- Session ID generator --> <bean ID ="sessionIdGenerator" class="com.xxx.controller.shiro.JavaUuidSessionIdGenerator"/>

    <bean id="redisCacheSessionDAO" class="com.xxx.controller.shiro.RedisCacheSessionDAO">
        <property name="sessionIdGenerator" ref="sessionIdGenerator"/>
    </bean>

    <bean id="tokenUserFilter" class="com.xxx.controller.shiro.TokenUserFilter"/ > <! -- Shiro filter --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <! -- Shiro's core security interface, this property is required --> <property name="securityManager" ref="securityManager"/ > <! -- If authentication fails, go to the configuration of the login page --> <property name="loginUrl" value="/index"/ > <! -- If permission authentication fails, jump to the specified page --> <property name="unauthorizedUrl" value="/index"/ > <! --> <property name="successUrl" value="/index"/>
        <property name="filters">
            <util:map>
                <entry key="token" value-ref="tokenUserFilter"></entry> </util:map> </property> <! -- Shiro connection constraint configuration, i.e., filter chain definition --> <property name="filterChainDefinitions">
            <value>
                /login/** = anon
                /logout/ * * =logout/** = token </value> </property> </bean> <! Ensure that beans that implement Shiro's internal lifecycle function execute --> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/ > <! -- Enable Shiro annotations --> <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
          depends-on="lifecycleBeanPostProcessor"/>
    <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
        <property name="securityManager" ref="securityManager"/>
    </bean>
</beans>
Copy the code

2. Login code

/** * Authentication information, mainly for user login, */ @override protected AuthenticationInfodoGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
      SsoUserNameToken ssoUserNameToken = (SsoUserNameToken) authenticationToken;
      LoginEntity loginEntity = ssoUserNameToken.getLoginEntity();
      UserInfo userInfo = null;
      try {
          userInfo = userService.login(loginEntity);
          Serializable id = SecurityUtils.getSubject().getSession().getId();
          userInfo.setToken((String) id);
          redisClient.set((String) id, SerializeUtil.serialize(userInfo), LOGIN_EXPIRE);
      } catch (RuhnnException e) {
          if (e.getErrorCode().equals(ErrorType.USER_NO_EXIST)) {
              throw new UnknownAccountException();
          } else if (e.getErrorCode().equals(ErrorType.PASSWORD_ERROR)) {
              throw new IncorrectCredentialsException();
          } else if(e.getErrorCode().equals(ErrorType.TOKEN_INVALID)) { throw new ExpiredCredentialsException(); }}if (loginEntity.getWay().intValue() == LoginWayEnum.Token_LOGIN.getWay().intValue()) {
          return new SimpleAuthenticationInfo(userInfo, userInfo.getToken(), getName());
      } else {
          returnnew SimpleAuthenticationInfo(userInfo, userInfo.getInfo().getPassword(), getName()); }}Copy the code

##3, Verify the code

/** * Authorized ** @Param principalCollection * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        UserInfo userInfo = (UserInfo) SecurityUtils.getSubject().getPrincipal();
        byte[] value = redisClient.get(userInfo.getToken());
        if(value ! = null) { userInfo = SerializeUtil.deserialize(value, UserInfo.class); } String key = SsoConstants.REDIS_ROLE_KEY + userInfo.getToken(); //getSession().getId() Set<String> allPermissions = new HashSet<>(); byte[] bytes = redisClient.get(key); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();if (bytes == null || bytes.length <= 0) {
            Set<FunctionDO> functionDOS = userService.queryUserFunction(userInfo.getInfo().getId(), userInfo.getWebId());
            if (CollectionUtils.isNotEmpty(functionDOS)) {
                Set<String> permissions = functionDOS.stream().map(FunctionDO::getUrl).collect(Collectors.toSet()); allPermissions.addAll(permissions); redisClient.set(key, SerializeUtil.serialize(permissions)); }}else {
            Set<String> permissions = SerializeUtil.deserialize(bytes, Set.class);
            allPermissions.addAll(permissions);
        }
        String ssoPublicLoginKey = SsoConstants.REDIS_PUBLIC_LOGIN_KEY;
        byte[] ssoPublicLoginValue = redisClient.get(ssoPublicLoginKey);
        if (ssoPublicLoginValue == null) {
            List<FunctionDO> publicLoginFunctionDOS = functionDao.queryPublicFunction(userInfo.getWebId());
            if(CollectionUtils.isNotEmpty(publicLoginFunctionDOS)) { Set<String> publicLoginPermissions = publicLoginFunctionDOS.stream().map(FunctionDO::getUrl).collect(Collectors.toSet()); redisClient.set(ssoPublicLoginKey, SerializeUtil.serialize(publicLoginPermissions)); allPermissions.addAll(publicLoginPermissions); }}else {
            Set<String> publicLoginPermissions = SerializeUtil.deserialize(ssoPublicLoginValue, Set.class);
            allPermissions.addAll(publicLoginPermissions);
        }
        info.setStringPermissions(allPermissions);
        return info;
    }
Copy the code

4, support distributed validation, rewrite sessionDAO

/**
 * @author star
 * @date 2018/5/22 下午3:49
 */
public class RedisCacheSessionDAO extends AbstractSessionDAO {

    @Resource
    private RedisClient redisClient;

    @Override
    protected Serializable doCreate(Session session) {

        Serializable sessionId = this.generateSessionId(session);
        this.assignSessionId(session, sessionId);
        redisClient.set(SsoConstants.REDIS_KEY + session.getId(), SerializeUtil.serialize(session), session.getTimeout() / 1000);
        return sessionId;
    }

    @Override
    protected Session doReadSession(Serializable serializable) {
        byte[] value = redisClient.get(SsoConstants.REDIS_KEY + serializable);
        return SerializeUtil.deserialize(value, Session.class);
    }

    @Override
    public void update(Session session) throws UnknownSessionException {
        if (session == null || session.getId() == null) {
            throw new NullPointerException("session is empty");
        }
        redisClient.set(SsoConstants.REDIS_KEY + session.getId(), SerializeUtil.serialize(session));

    }

    @Override
    public void delete(Session session) {
        if (session == null || session.getId() == null) {
            throw new NullPointerException("session is empty");
        }
        redisClient.remove(SsoConstants.REDIS_KEY + session.getId());
    }

    @Override
    public Collection<Session> getActiveSessions() {

        Set<byte[]> keys = redisClient.keys(SsoConstants.REDIS_KEY);
        if (CollectionUtils.isEmpty(keys)) {
            return null;
        }
        Collection<Session> collection = new HashSet<>();
        for (byte[] key : keys) {
            collection.add(SerializeUtil.deserialize(key, Session.class));
        }
        returncollection; }}Copy the code

5. Support two path styles

public class SsoPathMatcher implements PatternMatcher {
    @Override
    public boolean matches(String p, String source) {//pattern database,sourcePattern = pattern.compile (p); Matcher matcher = pattern.matcher(source);
        if (matcher.matches()) {
            return true;
        }
        return false; }}Copy the code
public class UrlPermission implements Permission {

    private static final Logger logger = LoggerFactory.getLogger(UrlPermission.class);

    private String url;

    public UrlPermission(String url){
        this.url = url;
    }

    @Override
    public boolean implies(Permission p) {
        if(! (p instanceof UrlPermission)){
            return false;
        }
        UrlPermission urlPermission = (UrlPermission) p;
        PatternMatcher patternMatcher = new RuhnnPathMatcher();
        logger.info("This. url(from wildcard data stored in the database) injected into Realm's authorization method =>" + this.url);
        logger.info("Urlpermission. url(from the link the browser is accessing) =>" +  urlPermission.url);
        System.out.println("This. url(from wildcard data stored in the database) injected into Realm's authorization method =>" + this.url);
        System.out.println("Urlpermission. url(from the link the browser is accessing) =>" +  urlPermission.url);
        boolean matches = patternMatcher.matches(this.url, urlPermission.url);
        returnmatches; }}Copy the code
public class UrlPermissionResolver implements PermissionResolver {
    @Override
    public Permission resolvePermission(String permissionString) {
        returnnew UrlPermission(permissionString); }}Copy the code

6. Override token acquisition

public class SsoSessionManager extends DefaultWebSessionManager {


    @Override
    protected Serializable getSessionId(ServletRequest httpRequest, ServletResponse response) {
        HttpServletRequest request = (HttpServletRequest) httpRequest;
        return request.getHeader("token"); }}Copy the code

Need to use SSO’s item filter

public class SsoFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException  { } @Override public voiddoFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        PrintWriter out = null;
        out = response.getWriter();
        String token = request.getHeader("token");
        if (StringUtils.isEmpty(token)) {
            token = request.getParameter("token");
        }
        if (StringUtils.isEmpty(token)) {
            out.write(JSON.toJSONString(new SsoResponse(ErrorType.INVALID_ARGUMENT)));
            return;
        }
        String uri = request.getRequestURI();
        JSONObject result = JSON.parseObject(HttpUtils.get("localhost:9999" + uri, token));
        if (result.getString("success").equals("true")) {
            filterChain.doFilter(servletRequest, servletResponse);
        } else {
            out.write(result.toJSONString());
            return;
        }
    }

    @Override
    public void destroy() {}}Copy the code

Six, summarized

The back-end logic is very simple, many things are put in front processing, the back-end only two requirements, one is the token value and the other is the domain name value, the token can determine the user permission, the domain name can determine the user’s first access to the menu permission. Therefore, the main problem in front end and short connection was that token could not be obtained. Because of cross-domain value penetration problem, there was an interface to exchange token with cookie, and the Controller layer was also changed. As the front end required cross-domain request, JSONP was used. The controller returns a piece of code that jSONP can execute. The above is my sso login system

Code: [email protected]: civism/civism – sso. Git