1. 开启Security

@EnableWebSecurity
@Configuration
public class UserWebSecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Resource
    private PasswordEncoder passwordEncoder;
    @Resource
    private LoginAuthenticationSuccessHandler successHandler;
    @Resource
    private LoginAuthenticationFailureHandler failureHandler;
    /**
     * 请求网页未登录时抛出的异常信息
     */
    @Resource
    private MessageAuthenticationEntryPoint entryPoint;
    @Resource
    private UserDetailsServiceImpl detailsService;
    @Resource
    private UserLoginAuthorizationTokenFilter tokenFilter;
    private final String[] AUTH_WHITELIST = new String[]{"/user/auth/login", "/user/login", "/oauth/login"};

    //认证管理器
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .userDetailsService(detailsService)
                .passwordEncoder(passwordEncoder);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf(AbstractHttpConfigurer::disable)
                .httpBasic(AbstractHttpConfigurer::disable)
                .formLogin(n -> n
                        .loginPage("/user/login")
                        .loginProcessingUrl("/user/auth/login")
                        .successHandler(successHandler)
                        .failureHandler(failureHandler))
                .logout(n -> n
                        .logoutUrl("/user/auth/logout"))
                .exceptionHandling(n->n.authenticationEntryPoint(entryPoint))
                .sessionManagement(n->n.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class)
                .authorizeRequests(n -> n
                        .antMatchers(AUTH_WHITELIST).permitAll()
                        .antMatchers("/user/api/**").authenticated());
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/admin/**");
    }

    /**
     * 注册密码加密器
     *
     * @return 密码加密器
     */
    @Bean
    public PasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

2. 配置登录验证UserDetailsService

package com.nineya.user.service;

import com.nineya.tool.text.CheckText;
import com.nineya.user.dao.UserMapper;
import com.nineya.user.entity.NineyaUserDetails;
import com.nineya.user.entity.User;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.Collection;

/**
 * @author 殇雪话诀别
 * 2020/11/22
 */
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
    @Resource
    private UserMapper userMapper;
    @Resource
    @Lazy
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = new User();
        if (CheckText.checkPhone(username)) {
            user.setPhone(username);
        } else if (CheckText.checkMail(username)) {
            user.setMail(username);
        } else if (CheckText.checkNineyaId(username)) {
            user.setNineyaId(username);
        } else {
            throw new UsernameNotFoundException(String.format("账号 %s 格式有误!", username));
        }
        user = userMapper.getUser(user);
        if (user == null) {
            throw new UsernameNotFoundException(String.format("账号 %s 不存在", username));
        }
        user.setPassword(passwordEncoder.encode(user.getPassword()));
        return new NineyaUserDetails(user);
    }
}

3. 自定义登录成功和失败处理器

登录成功处理器,这里只是简单demo,记录一下,直接响应了authentication,这里可以通过JWT打印个token给客户端,等等...

  1. ResponseResult是自定义的REsult接口统一风格,和security无关,无须理会;
  2. adminTokenUtil为自己封装的token管理器,生成了一个jwt凭证,同时将token的id在Redis存底,如果id不存在该token就不在生效,token的有效期不在jwt中存放,由服务端控制,这样是为了防止密码丢失,token无法提前失效的情况;
  3. ObjectMapper是springboot的json转换工具,可以直接使用@Resource注入。
package com.nineya.user.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.nineya.tool.restful.ResponseResult;
import com.nineya.user.entity.AdminDetails;
import com.nineya.user.util.AdminTokenUtil;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * @author 殇雪话诀别
 * 2020/11/22
 */
@Component
public class LoginAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Resource
    private AdminTokenUtil adminTokenUtil;
    @Resource
    private ObjectMapper mapper;
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
        Map<String, Object> data = new HashMap<>();
        ResponseResult result = ResponseResult.access(data);
        response.setContentType("application/json;charset=UTF-8");
        AdminDetails details = (AdminDetails) authentication.getPrincipal();
        data.put("admin_token", adminTokenUtil.createToken(details.getAid()));
        response.getWriter().write(mapper.writeValueAsString(result));
    }
}

登录失败处理器:

package com.nineya.user.handler;

import com.alibaba.fastjson.JSONObject;
import com.nineya.tool.restful.ResponseResult;
import com.nineya.tool.restful.ResultCode;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

/**
 * @author 殇雪话诀别
 * 2020/11/22
 */
@Component
public class LoginAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        ResponseResult result = new ResponseResult().setError(true);
        if (exception instanceof UsernameNotFoundException) {
            // 用户不存在
            result.setCode(ResultCode.SUCCESS.getCode());
            result.setMessage(exception.getMessage());
        } else if (exception instanceof BadCredentialsException) {
            // 密码错误
            result.setCode(ResultCode.SUCCESS.getCode());
            result.setMessage(exception.getMessage());
        } else if (exception instanceof LockedException) {
            // 用户被锁
            result.setCode(ResultCode.SUCCESS.getCode());
            result.setMessage(exception.getMessage());
        } else {
            // 系统错误
            result.setCode(ResultCode.SERVER_ERROR.getCode());
            result.setMessage(exception.getMessage());
        }
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(JSONObject.toJSONString(result));
    }
}

4. 创建访问失败处理器

访问接口时,因为登录失效,没有权限被访问等情况的处理器

package com.nineya.user.handler;

import com.alibaba.fastjson.JSONObject;
import com.nineya.tool.restful.ResponseResult;
import com.nineya.tool.restful.ResultCode;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author 殇雪话诀别
 * 2020/11/25
 *
 * 响应未登录消息
 */
@Component
public class MessageAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        ResponseResult result = new ResponseResult(ResultCode.FORBIDDEN).setError(true);
        if (authException instanceof UsernameNotFoundException) {
            // 用户不存在
            result.setMessage(authException.getMessage());
        } else if (authException instanceof BadCredentialsException) {
            // 密码错误
            result.setMessage(authException.getMessage());
        } else if (authException instanceof LockedException) {
            // 用户被锁
            result.setMessage(authException.getMessage());
        } else  if (authException instanceof InsufficientAuthenticationException) {
            // 用户token失效
            result.setMessage("用户未登录");

        } else {
            // 系统错误
            result.setCode(ResultCode.SERVER_ERROR.getCode());
            result.setMessage(authException.getMessage());
        }
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(JSONObject.toJSONString(result));
    }
}

5. 创建token解析过滤器

SecurityContextHolder.getContext().setAuthentication(authentication);表示添加授权,仅对当前请求有效,每个被security拦截的请求都会执行该过滤器;

简单示例,使用时可通过token查询数据库、Redis,或者解析JWT等方式判断登录状态,然后决定是否添加authentication

package com.nineya.user.filter;

import com.auth0.jwt.interfaces.DecodedJWT;
import com.nineya.tool.text.CheckText;
import com.nineya.user.entity.AdminDetails;
import com.nineya.user.util.AdminTokenUtil;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author 殇雪话诀别
 * 2020/11/26
 */
@Component
public class AdminLoginAuthorizationTokenFilter extends OncePerRequestFilter {
    @Resource
    private AdminTokenUtil adminTokenUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        final String token = httpServletRequest.getHeader("Admin-Authorization");
        if (CheckText.isEmpty(token)) {
            filterChain.doFilter(httpServletRequest, httpServletResponse);
            return;
        }
        DecodedJWT decodedJWT = adminTokenUtil.verifyToken(token);
        long aid = decodedJWT.getClaim("aid").asLong();
        long createTime = decodedJWT.getIssuedAt().getTime();
        if (adminTokenUtil.valid(aid, createTime)) {
            AdminDetails adminDetails = new AdminDetails(aid);
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(adminDetails, null, adminDetails.getAuthorities());
            // 在上下文中记录UserDetails
            SecurityContextHolder.getContext().setAuthentication(authentication);
            httpServletRequest.setAttribute("aid", aid);

        }
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }
}