spring AuthenticationInfo

Time:2021-5-13

background

HTTP protocol is stateless, that is, every message sent has no connection; This brings a problem: how to judge the login status of users? It’s impossible to re-enter the user name and password every time you request it. So people solve this problem by using the client cookie to save the sessionid and the server to save the session

  • Session is initially stored in memory, when the number of users is too large, the server needs to bear additional burden
  • Session is not conducive to horizontal expansion. For example, when you build a cluster, after using nginx to forward requests, you can’t determine which server each request is sent to

And, of course, there are solutions

  1. Nginx uses IP_ Hash mode. The user IP hash is calculated to get a fixed value to ensure that every request falls on the same server. Of course, this method is very stupid, because it means that once a server hangs up, the user will not be able to access it for a short time
  2. Save the session to the cache database, such as redis
  3. Use token to save login information and store token in cache database, such as redis
  4. When JWT is used to generate a token, the server will use the key to sign the token. Each time the client requests a token, it can directly verify whether the token is issued by the server. Of course, this method also has some defects: the expiration time of the token cannot be modified. This means that once the token is issued, it will be invalid, You can’t control the expiration time. In a sense, there are security risks. So sometimes a layer of redis is used to control the expiration time of the token

demo

  • MVC interceptor realizes login authentication
  • Shiro stand alone environment
  • Shiro redis cluster environment
  • jwt

1. MVC interceptor

Initialize interceptor configuration, filter login requests and static resources

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

 @Autowired
 private loginInteceptor loginInteceptor;
    
 @Override
 public void addResourceHandlers(ResourceHandlerRegistry registry) {
 registry.addResourceHandler("/statics/**")
 .addResourceLocations("classpath:/statics/");
        registry.addResourceHandler("/*.html")
 .addResourceLocations("classpath:/templates/");
    }
   
 @Override
 public void addInterceptors(InterceptorRegistry registry) {
 registry.addInterceptor(loginInteceptor).excludePathPatterns("/login.html", "/statics/**"
 ,"/shiro/login");
    }
}

User defined interceptor, if there is no login, it will be redirected to the login page

@Component
@Slf4j
public class loginInteceptor implements HandlerInterceptor {
 @Override
 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
 if (request.getSession().getAttribute("username") != null) {
 return true;
        }
 response.sendRedirect(request.getContextPath() + "/login.html");
        return false;
    }
}

2. Shiro stand alone environment

Shiro is an excellent security framework. Compared with spring security, it is easy to configure and widely used in springboot. In stand-alone architecture, session will be managed by Shiro

Shiro core module

1.subject: subject is the user who is currently accessing the system, which can be obtained through securityutils
2.realmShiro supports single realm authentication and multi realm authentication
3.SecurityManagerThe core manager of Shiro is responsible for authentication and authorization, and the manager obtains database data from relam
4.ShiroFilterFactoryBeanShiro interceptor is responsible for intercepting and releasing requests. After successful interception, the request will be returned to the manager for judgment

1. Login interface
The session has been managed by Shiro. Shirohttpsession implements the httpsession interface. Shiro has many built-in exceptions, which are not shown here

@PostMapping("login")
public Tr<?> shiroLogin(HttpSession httpSession,@RequestBody UserEntity entity) {
 log.info("session:{}", new Gson().toJson(httpSession));
    Subject subject = SecurityUtils.getSubject();
    try {
 subject.login(new UsernamePasswordToken(entity.getName(), entity.getPassword()));
        Return new tr < > (200, "login successful");
    } catch (Exception e) {
 Return new tr < > ("login failed");
    }
}

2. User defined realm as data interaction layer
Override dogetauthenticationinfo for authentication
Note that Shiro uses char array to store password. Here, it needs to be converted to string

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
 UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
    String password = String.valueOf(usernamePasswordToken.getPassword());
    UserEntity entity = userService.getOne(
 new QueryWrapper<UserEntity>()
 .eq("name", usernamePasswordToken.getUsername())
 );
    if (entity == null) {
 Throw new unknownaccountexception ("account does not exist");
    } else {
 if (!password.equals(entity.getPassword())) {
 Throw new incorrectcredentialsexception ("password error");
        }
 } return new SimpleAccount(authenticationToken.getPrincipal(), authenticationToken.getCredentials(), getName());
}

3. Inject Shiro manager and interceptor

@Bean
public CustomRealm customRealm() {
 return new CustomRealm();
}

/**
 *Manager, inject custom realm
 */
@Bean("securityManager")
public SessionsSecurityManager securityManager(CustomRealm customRealm) {
 DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    securityManager.setRealm(customRealm);
    return securityManager;
}


/**
 *Shiro filter, factory injection Manager
 */
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
 ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    shiroFilterFactoryBean.setSecurityManager(securityManager);
    Map<String, String> filterMap = new LinkedHashMap<>();
//Let go of the interception
filterMap.put("/shiro/login/**","anon");
filterMap.put("login.html","anon");
//Let go of static resources
filterMap.put("/statics/**","anon");
//Intercept all
filterMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
//Default authentication path default login.jsp
shiroFilterFactoryBean.setLoginUrl("/login.html");
return shiroFilterFactoryBean;
}

3. Shiro redis cluster environment

First of all, we need to introduce an additional Shiro redis plug-in to help us realize using redis as the cache manager of Shiro

<dependency>
 <groupId>org.crazycake</groupId>
 <artifactId>shiro-redis</artifactId>
 <version>3.1.0</version>
</dependency>

The built-in iredimanager of crazycake has four implementation classes, as shown in the figure. Select one according to the actual situation
spring AuthenticationInfo

To configure the cache processor for the relam, you can also set it directly for the security manager. It depends on the fine-grained control

@Bean("customRealm")
public CustomRealm customRealm() {
 //redis
 RedisManager redisManager = new RedisManager();
    redisManager.setHost("127.0.0.1");
    redisManager.setPort(6380);
    //Shiro cache manager
    RedisCacheManager redisCacheManager = new RedisCacheManager();
    //Unique identification
    redisCacheManager.setPrincipalIdFieldName("id");
    redisCacheManager.setRedisManager(redisManager);
    Log. Info ("redis cache manager: {}", new gson (). Tojson (rediscache manager));
    CustomRealm customRealm = new CustomRealm();
    //Turn on global caching
    customRealm.setCachingEnabled(true);
    //Enable authentication cache
    customRealm.setAuthenticationCachingEnabled(true);
    customRealm.setCacheManager(redisCacheManager);
    return customRealm;
}

After opening the cache, calling the subject’s login interface will give priority to using the cache data instead of querying mysql

private AuthenticationInfo getCachedAuthenticationInfo(AuthenticationToken token) {
 AuthenticationInfo info = null;
    Cache<Object, AuthenticationInfo> cache = getAvailableAuthenticationCache();
    if (cache != null && token != null) {
    log.trace("Attempting to retrieve the AuthenticationInfo from cache.");
        Object key = getAuthenticationCacheKey(token);
        info = cache.get(key);
        if (info == null) {
    log.trace("No AuthorizationInfo found in cache for key [{}]", key);
        } else {
    log.trace("Found cached AuthorizationInfo for key [{}]", key);
        }
 }
 return info;
}

4.JWT

JSON web token: the server issues the token according to the key and sets the expiration time. The client carries the token when accessing, and can directly determine whether it is issued by the current server according to the key. This feature of JWT is also often used in single sign on and other scenarios

JWT is composed of header, payload and signature. The header stores the token type and signature algorithm. Payload stores insensitive business information. The signature is generated by the back end according to the key. In practical application, it will be transmitted after encoding with Base64

@PostMapping("login")
public Tr<?> jwtLogin(HttpSession httpSession,@RequestBody UserEntity entity) {

UserEntity userEntity = userService.getOne(
 new QueryWrapper<UserEntity>().eq("name", entity.getName()));
    
 If (userentity = = null) {return new tr < > (account does not exist);}
 
 if(!entity.getPassword()
 .equals(userEntity.getPassword()))
 {return new tr < > (password error);}
 
 //If the account password is correct, the token will be generated
 String jwtToken = JwtUtil.sign(entity.getName());
Log. Info ("get token: {}", new gson (). Tojson (jwttoken));
Return new tr < > (200, jwttoken, "login successful");
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

String token = request.getHeader("token");
Log. Info ("get token: {}", token) ";

if(StringUtils.isNotBlank(JwtUtil.verify(token))){
 return true;
 }
 
response.sendRedirect(request.getContextPath() + "/login.html");
return false;
}