This article explains how to use SpringBoot version: 2.2.6.RELEASE, Spring Security version: 5.2.2.RELEASE

There are two popular Security frameworks in Java, Apache Shiro and Spring Security. Shiro is not very friendly to back-end separation projects, and Spring Security is chosen. SpringBoot provides the official Spring-boot-starter-Security, which can be easily integrated into the SpringBoot project. However, enterprise-level use still needs a slight transformation. This article implements the following functions:

  • Exception Handling when anonymous users access resources without permission
  • Whether the login user has the permission to access resources
  • Distributed session sharing based on Redis
  • Handling session timeout
  • Limit the maximum number of users who can log in to an account at the same time (top number)
  • Returns JSON if the login succeeds or fails
  • Supports three token storage locations: cookie, HTTP header,request parameter

Quick use and introduction of dependencies

<! --spring security-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<! -- spring session redis -->
<dependency>
  <groupId>org.springframework.session</groupId>
  <artifactId>spring-session-data-redis</artifactId>
</dependency>
Copy the code

Spring-boot-starter-security is used to integrate Spring Security. Spring-session-data-redis integrates Redis and spring-session.

Customized access to Spring Security

The purpose of Using Spring Security is to write as little code as possible to achieve more functionality. In customizing Spring Security, the idea is to rewrite a feature and then configure it.

  • For example, if you want to check your own user table to log in, implement the UserDetailsService interface.
  • Login separation projects such as before and after the end, success and failure to return json, then achieve AuthenticationFailureHandler/AuthenticationSuccessHandler interface;
  • For example, to extend the token store location, it implements the HttpSessionIdResolver interface;
  • And so on…

Finally, configure the above changes into Security. Routine is this routine, below let’s practice.

Don’t bb, show me code.

1. Handle that anonymous users have no access

The AuthenticationEntryPoint interface can handle the exceptions when anonymous users access resources without permission, as follows:

@Slf4j
@Component
public class AnonymousAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        log.warn("User needs login, access to [{}] failed, AuthenticationException={}", request.getRequestURI(), e); ServletUtils.render(request, response, RestResponse.fail(ResponseCode.USER_NEED_LOGIN)); }}public class ServletUtils {

    /** * render to client **@paramObject The entity class to be rendered is automatically converted to JSON */
    public static void render(HttpServletRequest request, HttpServletResponse response, Object object) throws IOException {
        // Allow cross-domain
        response.setHeader("Access-Control-Allow-Origin"."*");
        // Allow custom request header token(allow head to cross domain)
        response.setHeader("Access-Control-Allow-Headers"."token, Accept, Origin, X-Requested-With, Content-Type, Last-Modified");
        response.setHeader("Content-type"."application/json; charset=UTF-8"); response.getWriter().print(JSONUtil.toJsonStr(object)); }}Copy the code

Note that the commerce method is also entered when a program encounters an exception error (such as 500).

2. User login authentication logic based on the database

The system searches the database for the login user’s information (such as password), role, and permission, and then returns an entity of the type of UserDetails. The security system automatically determines the login success or failure of the user based on the password and user status (whether the user is locked, started, stopped, expired, etc.).

@Slf4j
@Component
public class DefaultUserDetailsService implements UserDetailsService {

    @Autowired
    private SystemService systemService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if (StrUtil.isBlank(username)) {
            log.info("Login user: {} does not exist", username);
            throw new UsernameNotFoundException("Login user:" + username + "Doesn't exist.");
        }

        // check the password
        UserVO userVO = systemService.loadUserByUsername(username);
        if (ObjectUtil.isNull(userVO) || StrUtil.isBlank(userVO.getUserId())) {
            log.info("Login user: {} does not exist", username);
            throw new UsernameNotFoundException("Login user:" + username + "Doesn't exist.");
        }
        return newLoginUser(userVO, IpUtils.getIpAddr(ServletUtils.getRequest()), LocalDateTime.now(), LoginType.PASSWORD); }}/** * Extend user information **@author songyinyin
 * @date 2020/3/14 下午 05:29
 */
@Data
public class LoginUser implements UserDetails.CredentialsContainer {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    /** * user */
    private UserVO user;

    /** * Login IP address */
    private String loginIp;

    /** * Login time */
    private LocalDateTime loginTime;

    /** * Login type */
    private LoginType loginType;

    public LoginUser(a) {}public LoginUser(UserVO user, String loginIp, LocalDateTime loginTime, LoginType loginType) {
        this.user = user;
        this.loginIp = loginIp;
        this.loginTime = loginTime;
        this.loginType = loginType;
    }

    public LoginUser(UserVO user, String loginIp, LocalDateTime loginTime, String loginType) {
        this.user = user;
        this.loginIp = loginIp;
        this.loginTime = loginTime;
        this.loginType = LoginType.valueOf(loginType);
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    @Override
    public String getPassword(a) {
        return user.getPassword();
    }

    @Override
    public String getUsername(a) {
        return user.getUserName();
    }

    /** * Whether the account has not expired, expiration cannot verify */
    @Override
    public boolean isAccountNonExpired(a) {
        return true;
    }

    /** * Specifies whether to unlock the user. Locked users cannot be authenticated * 

* password locked *

*/
@Override public boolean isAccountNonLocked(a) { return ObjectUtil.equal(user.getPwdLockFlag(), LockFlag.UN_LOCKED); } /** * indicates whether the user's credentials (password) have expired. Expired credentials prevent authentication */ @Override public boolean isCredentialsNonExpired(a) { return true; } /** * Whether the user is enabled or disabled. Disabled users cannot be authenticated. * / @Override public boolean isEnabled(a) { return ObjectUtil.equal(user.getStopFlag(), StopFlag.ENABLE); } /** * After authentication is complete, the password is erased */ @Override public void eraseCredentials(a) { user.setPassword(null); }}Copy the code

LoginUser also implements the CredentialsContainer interface. After the user is authenticated successfully, the password is erased and returned to the front end.

3. Handle the successful login

After a successful login, log the login and return the authenticated user to the front end

@Slf4j
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

        // The TODO login succeededServletUtils.render(request, response, RestResponse.success(authentication)); }}Copy the code

4. Troubleshoot the login failure

After a login failure, you can distinguish the login failure based on different AuthenticationExceptions. In this case, you need to print logs and return information to the front end according to service requirements. For example, the requirement is to return a login failure regardless of any error. In this example, the login failure distinction is made.

@Slf4j
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        RestResponse result;
        String username = UserUtil.loginUsername(request);
        if (e instanceof AccountExpiredException) {
            // The account has expired
            log.info("[Login failed] - User [{}] account expired", username);
            result = RestResponse.build(ResponseCode.USER_ACCOUNT_EXPIRED);

        } else if (e instanceof BadCredentialsException) {
            // The password is incorrect
            log.info("[login failed] - user [{}] password error", username);
            result = RestResponse.build(ResponseCode.USER_PASSWORD_ERROR);

        } else if (e instanceof CredentialsExpiredException) {
            // The password has expired
            log.info("[Login failed] - User [{}] password expired", username);
            result = RestResponse.build(ResponseCode.USER_PASSWORD_EXPIRED);

        } else if (e instanceof DisabledException) {
            // The user is disabled
            log.info("[Login failed] - User [{}] is disabled", username);
            result = RestResponse.build(ResponseCode.USER_DISABLED);

        } else if (e instanceof LockedException) {
            // The user is locked
            log.info("[Login failed] - User [{}] is locked", username);
            result = RestResponse.build(ResponseCode.USER_LOCKED);

        } else if (e instanceof InternalAuthenticationServiceException) {
            // Internal error
            log.error(String.format("[logon failed] - [%s] Internal error", username), e);
            result = RestResponse.fail(ResponseCode.USER_LOGIN_FAIL);

        } else {
            // Other errors
            log.error(String.format("[login failed] - [%s] Other error", username), e);
            result = RestResponse.fail(ResponseCode.USER_LOGIN_FAIL);
        }
        // TODO failed to log inServletUtils.render(request, response, result); }}Copy the code

5. Exit the login callback

Similar to a successful or failed login, log and return front-end JSON.

@Slf4j
@Component
public class LogoutSuccessHandler implements org.springframework.security.web.authentication.logout.LogoutSuccessHandler {

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

        // TODO logs out successfullyServletUtils.render(request, response, RestResponse.success()); }}Copy the code

6. Handle login timeout

After a user logs in, the user is automatically logged out when the timeout period (session expires) expires

@Slf4j
@Component
public class InvalidSessionHandler implements InvalidSessionStrategy {

    @Override
    public void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        log.info("User login timed out, access to [{}] failed", request.getRequestURI()); ServletUtils.render(request, response, RestResponse.fail(ResponseCode.USER_LOGIN_TIMEOUT)); }}Copy the code

7. The number of users who log in to the same account at the same time is limited

Such as a user login session number at the same time, more than the set up of the system, and the vernacular is the top number, at this time will be handled by SessionInformationExpiredStrategy. In addition, the online user is proposed by the administrator, also triggered.

@Slf4j
@Component
public class SessionInformationExpiredHandler implements SessionInformationExpiredStrategy {

    @Override
    public void onExpiredSessionDetected(SessionInformationExpiredEvent sessionInformationExpiredEvent) throws IOException, ServletException { ServletUtils.render(sessionInformationExpiredEvent.getRequest(), sessionInformationExpiredEvent.getResponse(), RestResponse.fail(ResponseCode.USER_MAX_LOGIN)); }}Copy the code

8. Implement user-defined authentication

After a user logs in, how can you determine whether the user has permission to access the resource? Remember we in ** [2. Database based user login authentication logic] **, from the database will be the user’s authority role to find out, for our present authentication to provide the basis.

@Slf4j
@Service("ps")
public class PermissionService {

    public boolean permission(String permission) {
        LoginUser loginUser = UserUtil.loginUser();
        for (String userPermission : loginUser.getUser().getPermissions()) {
            if (permission.matches(userPermission)) {
                return true; }}if (log.isDebugEnabled()) {
            log.debug("User userId = {}, the userName = {} permissions to access ({}), the user has permissions: {}, access", loginUser.getUser().getUserId(),
                    loginUser.getUsername(), permission, loginUser.getUser().getPermissions());
        } else {
            log.info("UserId ={}, userName={}", loginUser.getUser().getUserId(), loginUser.getUsername(), permission);
        }
        return false; }}@RestController
public class UserController {

    @Autowired
    protected IUserService userService;

    @GetMapping("/user/page")
    @ApiOperation(value = "Paging query user")
    @PreAuthorize("@ps.permission('system:user:page')")
    public TableResponse<UserVO> page(a) {
        IPage<User> page = userService.getPage();

        List<UserVO> userVOList = page.getRecords().stream().map(e -> {
            UserVO userVO = new UserVO();
            BeanUtils.copyPropertiesIgnoreNull(e, userVO);
            return userVO;
        }).collect(Collectors.toList());

        returnTableResponse.success(page.getTotal(), userVOList); }}Copy the code

Use the @preauthorize annotation to protect applied resources. However, need to configure the @ EnableGlobalMethodSecurity (prePostEnabled = true) to make @ PreAuthorize to take effect

9. The login user does not have access permission

The AccessDeniedHandler is needed to handle the user’s login but not enough permission to access certain resources

@Slf4j
@Component
public class LoginUserAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { ServletUtils.render(request, response, RestResponse.build(ResponseCode.NO_AUTHENTICATION)); }}Copy the code

10. Customize Session resolvers

The official implementation of Cookie and Session parsing, in the actual project, also encounter the situation of token concatenation URL, this can be HttpSessionIdResolver interface

/** * Support sessionId to cookie, header and request parameter **@author songyinyin
 * @date 2020/3/18 下午 05:53
 */
@Slf4j
@Service("httpSessionIdResolver")
public class RestHttpSessionIdResolver implements HttpSessionIdResolver {

    public static final String AUTH_TOKEN = "GitsSessionID";

    private String sessionIdName = AUTH_TOKEN;

    private CookieHttpSessionIdResolver cookieHttpSessionIdResolver;

    public RestHttpSessionIdResolver(a) {
        initCookieHttpSessionIdResolver();
    }

    public RestHttpSessionIdResolver(String sessionIdName) {
        this.sessionIdName = sessionIdName;
        initCookieHttpSessionIdResolver();
    }

    public void initCookieHttpSessionIdResolver(a) {
        this.cookieHttpSessionIdResolver = new CookieHttpSessionIdResolver();
        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
        cookieSerializer.setCookieName(this.sessionIdName);
        this.cookieHttpSessionIdResolver.setCookieSerializer(cookieSerializer);
    }


    @Override
    public List<String> resolveSessionIds(HttpServletRequest request) {
        // cookie
        List<String> cookies = cookieHttpSessionIdResolver.resolveSessionIds(request);
        if (CollUtil.isNotEmpty(cookies)) {
            return cookies;
        }
        // header
        String headerValue = request.getHeader(this.sessionIdName);
        if (StrUtil.isNotBlank(headerValue)) {
            return Collections.singletonList(headerValue);
        }
        // request parameter
        String sessionId = request.getParameter(this.sessionIdName);
        return(sessionId ! =null)? Collections.singletonList(sessionId) : Collections.emptyList(); }@Override
    public void setSessionId(HttpServletRequest request, HttpServletResponse response, String sessionId) {
        log.info(AUTH_TOKEN + "= {}", sessionId);
        response.setHeader(this.sessionIdName, sessionId);
        this.cookieHttpSessionIdResolver.setSessionId(request, response, sessionId);
    }

    @Override
    public void expireSession(HttpServletRequest request, HttpServletResponse response) {
        response.setHeader(this.sessionIdName, "");
        this.cookieHttpSessionIdResolver.setSessionId(request, response, ""); }}Copy the code

Configure Spring Security

After all this preparation, it was finally time to configure, and Spring Security made it easy with builder mode.

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private DefaultUserDetailsService userDetailsService;
    /** * successful logout processing */
    @Autowired
    private LoginFailureHandler loginFailureHandler;
    /** * Successful login processing */
    @Autowired
    private LoginSuccessHandler loginSuccessHandler;
    /** * successful logout processing */
    @Autowired
    private LogoutSuccessHandler logoutSuccessHandler;
    /** * Unlogged processing */
    @Autowired
    private AnonymousAuthenticationEntryPoint anonymousAuthenticationEntryPoint;
    /** * Timeout processing */
    @Autowired
    private InvalidSessionHandler invalidSessionHandler;
    /** ** handle */
    @Autowired
    private SessionInformationExpiredHandler sessionInformationExpiredHandler;
    /** * The login user does not have permission to access resources */
    @Autowired
    private LoginUserAccessDeniedHandler accessDeniedHandler;

    /** * Configure the authentication mode@param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService);
    }

    /** * HTTP related configuration, including login and logout, exception handling, session management, etc. **@param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable();
        http.authorizeRequests()
                // Release interface
                .antMatchers(GitsResourceServerConfiguration.AUTH_WHITELIST).permitAll()
                // All requests other than the above require authentication
                .anyRequest().authenticated()
                // Exception handling (permission denied, login invalid, etc.)
                .and().exceptionHandling()
                .authenticationEntryPoint(anonymousAuthenticationEntryPoint)// Handle exceptions when anonymous users access resources without permission
                .accessDeniedHandler(accessDeniedHandler)// The login user does not have permission to access resources
                / / login
                .and().formLogin().permitAll()// Allow all users
                .successHandler(loginSuccessHandler)// Logon success processing logic
                .failureHandler(loginFailureHandler)// Logon failure processing logic
                / / logout
                .and().logout().permitAll()// Allow all users
                .logoutSuccessHandler(logoutSuccessHandler)// Logout successfully processed logic
                .deleteCookies(RestHttpSessionIdResolver.AUTH_TOKEN)
                // Session management
                .and().sessionManagement().invalidSessionStrategy(invalidSessionHandler) // Timeout processing
                .maximumSessions(1)// Maximum number of concurrent users of the same account
                .expiredSessionStrategy(sessionInformationExpiredHandler) // handle the top number; }}Copy the code

@ EnableWebSecurity annotations to enable Spring Security, @ EnableGlobalMethodSecurity (prePostEnabled = true) used to make @ PreAuthorize to take effect. Some details are also written in the comments of the code, which makes it easier to see straight.

Post request IP :port/login, you can see the login result, as follows:

Afterword.

At this point, you should have a fairly well-configured security framework, and all the code for this article is open source and tested.

Address: gitee.com/songyinyin/…

By following the steps in this article, you have taken the initial steps of SpringSecurity, which gives you a general overview of the Security framework. Of course, there are bound to be some questions, such as why the login interface is not visible from beginning to end. UserDetailsService#loadUserByUsername()

Leave a comment about your initial confusion with SpringSecurity


Wechat search “read diaoyy”, the first time to read quality original good articles.

Original is not easy, read to the end, please like this text, thank you very much.