学习思路
- 认证流程
- 基于Spring-Security原理讲解
- 用户名密码模式具体代码实现
- 根据用户名密码模式思考短信验证码模式
一、认证流程
用户登录、认证流程
- 用户输入用户名密码请求后台服务
- 后台验证用户名密码是否正确
- 正确返回token,错误抛出异常
- 用户拿到token请求非白名单服务
- 如果token正常访问成功,如果token异常访问失败
题外话:有没有刚刚入门写demo的感觉
二、基于Spring-Security原理讲解
首先看一下用户名密码认证的源码时序图
解释:
- AuthencationManager对usernamePasswordAuthencationToken进行认证
- Manager又将token交给了与该token相匹配的AuthenticationProvider(可多个provider)
- Provider调用本地UserDetailService获取用户信息
- provider进行密码校验
- 校验通过发送成功事件,进入成功后处理,失败抛出异常
三、用户名密码模式具体代码实现
由于Security默认实现了用户名密码模式
1、引入jar包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2、编写Filter集成AbstractAuthenticationProcessingFilter,实现attemptAuthentication方法返回Authentication即可
public class UsernamePasswordFilter extends AbstractAuthenticationProcessingFilter {
@Autowired
LoginExtendProperty loginExtendProperty;
@Autowired
UserExtendService userExtendService;
/**
* 重新定义登陆地址
* @param authenticationManager
*/
public UsernamePasswordFilter(AuthenticationManager authenticationManager, String loginUrl) {
super(new AntPathRequestMatcher(loginUrl, HttpMethod.POST.name()));
setAuthenticationManager(authenticationManager);
setRequiresAuthenticationRequestMatcher(new RequestMatcher(MatcherRule.USERNAME_PASSWORD.getRuleName()));
}
/**
* 从body取用户名密码
* @return
* @throws AuthenticationServiceException
* @throws IOException
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationServiceException, IOException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
if (!userExtendService.validate(request)){
throw new AuthenticationServiceException("业务验证异常");
}
String json = StreamUtils.copyToString(request.getInputStream(), Charset.forName("UTF-8"));
logger.info(json);
JSONObject jsonObject;
if (null !=json && !"".equals(json) && json.contains(loginExtendProperty.getUsername()) && json.contains(loginExtendProperty.getPassword())){
jsonObject=JSONObject.parseObject(json);
}else {
throw new AuthenticationServiceException("用户名或密码错误");
}
return this.getAuthenticationManager().authenticate(new UsernamePasswordAuthenticationToken(
jsonObject.get(loginExtendProperty.getUsername()), jsonObject.get(loginExtendProperty.getPassword())
));
}
}
其中UsernamePasswordAuthenticationToken内部已经提供实现,我们只需要将用户名密码从request里拿出来new对象即可
3、将配置Filter,编写config继承WebSecurityConfigurerAdapter
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@ComponentScan(basePackages = {"com.murphy.security"})
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthFilterProperty jwtAuthFilterProperty;
@Autowired
LoginExtendProperty loginExtendProperty;
@Autowired
private SimpleAuthenticatingSuccessHandler simpleAuthenticatingSuccessHandler;
@Autowired
private SimpleAuthenticatingFailureHandler simpleAuthenticatingFailureHandler;
@Autowired
UserExtendService userExtendService;
private Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* HTTP请求安全处理
* token请求授权
*
* @param httpSecurity .
* @throws Exception .
*/
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
// 由于使用的是JWT,我们这里不需要csrf
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry expressionInterceptUrlRegistry
= httpSecurity.csrf().disable()
//未授权处理
.exceptionHandling().authenticationEntryPoint((request, response, authException) -> {
logger.error("未授权uri={}",request.getRequestURI());
response.setHeader("Access-Control-Allow-Origin", "*");
response.setStatus(200);
Map<String,Object> map=new HashMap<>(3);
map.put("code","401");
map.put("message","未授权uri="+request.getRequestURI());
map.put("data",Boolean.FALSE);
response.setCharacterEncoding("UTF-8");
JSONObject.writeJSONString(response.getWriter(),map);
})
// 基于token,所以不需要session
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().authorizeRequests();
//白名单,无需token
String[] urls = jwtAuthFilterProperty.getExceptUrl().split(",");
for (String url : urls) {
// 对于获取token的rest api要允许匿名访问
expressionInterceptUrlRegistry.antMatchers(url).permitAll();
}
expressionInterceptUrlRegistry
.antMatchers(HttpMethod.OPTIONS).permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
// 添加JWT filter
//将token验证添加在密码验证前面
httpSecurity.addFilterBefore(getJwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
httpSecurity.addFilterBefore(getUsernamePasswordFilter(), JwtAuthenticationTokenFilter.class);
httpSecurity.authenticationProvider(new SmsAuthenticationProvider(userExtendService)).addFilterBefore(getSmsFilter(),UsernamePasswordFilter.class);
// 禁用缓存
httpSecurity.headers().cacheControl();
}
/**
* 密码加密
* @author dongsufeng
* @return org.springframework.security.crypto.password.PasswordEncoder
* @date 2019/9/17 18:39
*/
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
public JwtAuthenticationTokenFilter getJwtAuthenticationTokenFilter() throws Exception {
return new JwtAuthenticationTokenFilter();
}
@Bean
public AuthenticationManager getManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public UsernamePasswordFilter getUsernamePasswordFilter() throws Exception {
UsernamePasswordFilter userLoginFilter = new UsernamePasswordFilter(getManagerBean(), loginExtendProperty.getLoginUrl());
userLoginFilter.setAuthenticationSuccessHandler(simpleAuthenticatingSuccessHandler);
userLoginFilter.setAuthenticationFailureHandler(simpleAuthenticatingFailureHandler);
return userLoginFilter;
}
}
解释:
- configure()方法是http安全请求处理,主要配置了一些白名单,那些拦截,Filter及顺序
- getManagerBean()是获取AuthenticationManager
- getUsernamePasswordFilter()里边配置了执行成功和失败的Handler
4、编写执行成功,失败的Handler
/**
* 登陆成功后处理
* @author dongsufeng
* @date 2019/12/02 15:01
* @version 1.0
*/
@Component
public class SimpleAuthenticatingSuccessHandler implements AuthenticationSuccessHandler {
private static ObjectMapper globalObjectMapper = new ObjectMapper();
@Autowired
private JwtService jwtService;
static {
globalObjectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
}
private Logger logger = LoggerFactory.getLogger(SimpleAuthenticatingSuccessHandler.class);
private static void clearAuthenticationAttributes(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session == null) {
return;
}
session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
}
/**
* 登陆成功返回token给用户
* @param authentication
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
Map<String, Object> result = new HashMap<>();
logger.info("登录成功返回对象==={}",JSONObject.toJSONString(authentication.getPrincipal()));
result.put("token", jwtService.generateToken(authentication));
response.setStatus(HttpStatus.OK.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
globalObjectMapper.writeValue(response.getWriter(), result);
clearAuthenticationAttributes(request);
}
}
解释:
- 主要实现AuthenticationSuccessHandler接口实现onAuthenticationSuccess方法
- 成功后生成token:jwtService.generateToken(authentication)
- 封装token返回
- 异常情况实现AuthenticationFailureHandler接口跟成功差不多
5、生成token,看注释,可以适当将用户信息放入
/**
* 生成令牌
*
* @param .
* @return .
*/
public String generateToken(Authentication authentication) {
return Jwts.builder()
//jwt签发者
.setIssuer(jwtPayloadProperties.getIssuer())
// jwt所面向的用户
.setSubject(authentication.getName())
//接收jwt的一方
.setAudience(jwtPayloadProperties.getAudience())
//生效时间
.setNotBefore(new Date(System.currentTimeMillis() - jwtPayloadProperties.getNotBeforeMinute() * 60 * 1000))
//失效时间
.setExpiration(new Date(System.currentTimeMillis() + jwtPayloadProperties.getExpirationMinute() * 60 * 1000))
//签发时间
.setIssuedAt(new Date())
//秘钥签名
.signWith(SignatureAlgorithm.HS512, jwtPayloadProperties.getSecret())
.claim("userDTO", JSON.toJSONString(authentication.getPrincipal()))
.compact();
}
6、作为使用者应该实现UserDetailsService接口实现loadUserByUsername()
@Component
@Log4j2
public class UserDetailsService implements UserExtendService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if ("18888888888".equals(username)) {
User user = new User("18888888888", PasswordEncoderFactories.createDelegatingPasswordEncoder().encode("123456"), "userid1111101111");
user.setMobile("13333333333");
return user;
}else {
return null;
}
}
}
这里主要通过username查询用户信息,这个必须实现,主要用于providr里获取用户信息
7、用户拿到token后可以将token放入header然后请求相应的服务了,进入验证环境
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private JwtService jwtService;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtAuthFilterProperty jwtAuthFilterProperty;
/**
* 过滤器逻辑
*
* @param request .
* @param response .
* @param chain .
* @throws ServletException .
* @throws IOException ,
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
String authHeader = request.getHeader(this.jwtAuthFilterProperty.getHeader());
String tokenHead = this.jwtAuthFilterProperty.getTokenHead();
if (authHeader != null && authHeader.startsWith(tokenHead)) {
String authToken = authHeader.substring(tokenHead.length());
if (jwtService.validateToken(authToken)) {
Claims claimsFromToken = jwtService.getClaimsFromToken(authToken);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(claimsFromToken.getSubject(), null, null);
String userDTO = claimsFromToken.get("userDTO", String.class);
log.info("=======lanjie={}",userDTO);
request.setAttribute("userInfoDTO", userDTO);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
chain.doFilter(request, response);
}
}
解释:
- 正常实现Filter,然后对token进行解析验证
- 将UsernamePasswordAuthenticationToken放入SecurityContext即可
至此整个用户名密码验证流程结束
四、根据用户名密码模式思考短信验证码模式
我们了解了用户名密码整体流程之后,再来看如果是短信验证码该怎么做
我们来模仿整个看看思路:
- 写一个短信的Token跟UsernamePasswordAuthenticationToken,用于设置一些用户名等参数
- 写一个provider去获取用户信息,并进行相关的校验
然后其它的就跟用户名密码一样了,,
1、编写SmsAuthonticationToken继承AbstractAuthenticationToken(直接复制UsernamePasswordAuthonticationToken将里边的密码参数删掉就可以了)
详情可看源码:com.murphy.security.token.SmsAuthenticationToken
2、编写provider实现AuthenticationProvider
public class SmsAuthenticationProvider implements AuthenticationProvider {
// private UserDetailsService userDetailsService;
private UserExtendService userExtendService;
public SmsAuthenticationProvider(){}
public SmsAuthenticationProvider( UserExtendService userExtendService){
// this.userDetailsService=userDetailsService;
this.userExtendService = userExtendService;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsAuthenticationToken authenticationToken = (SmsAuthenticationToken) authentication;
/**
* 调用 {@link UserDetailsService}
*/
UserDetails user = userExtendService.loadUserByUsername((String)authenticationToken.getPrincipal());
if (Objects.isNull(user)) {
throw new InternalAuthenticationServiceException("手机号不存在");
}
if (!userExtendService.validate(authenticationToken.getRequest())){
throw new InternalAuthenticationServiceException("验证码错误");
}
SmsAuthenticationToken authenticationResult = new SmsAuthenticationToken(user, user.getAuthorities());
authenticationResult.setDetails(authenticationToken.getDetails());
return authenticationResult;
}
@Override
public boolean supports(Class<?> authentication) {
return SmsAuthenticationToken.class.isAssignableFrom(authentication);
}
}
这里主要看userExtendService.validate(authenticationToken.getRequest())
验证码校验属于业务,有各种各样的实现方案(比如放redis里)这里可以提供接口,让使用方实现接口,我们在这里调用一下
public interface UserExtendService extends UserDetailsService{
/**
* 业务校验
* @param request
* @return
*/
default boolean validate(HttpServletRequest request){
return true;
};
}
然后再根据用户名密码那套流程,将usernamePasswordAuthenticationToken换成SmsAuthenticationToken;将SmsAuthenticationProvider放入配置即可
具体配置文件可看源码:com.murphy.security.config.WebSecurityConfig
这样如果再有其它的认证方式,画瓢即可,验证可以给默认,然后再暴露给使用者,可以重写
源码地址:https://gitee.com/carpentor/spring-cloud-example
公众号主要记录各种源码、面试题、微服务技术栈,帮忙关注一波,非常感谢