网站首页 > 技术文章 正文
验证码(图形、短信、邮箱)、token机制对于系统的安全性已经是老生常谈;
本文将结合spring-security快速实现Google图形验证码、token的安全性校验。
技术储备
1、UserDetailsService接口
/**
* Core interface which loads user-specific data.
* <p>
* It is used throughout the framework as a user DAO and is the strategy used by the
* {@link org.springframework.security.authentication.dao.DaoAuthenticationProvider
* DaoAuthenticationProvider}.
*
* <p>
* The interface requires only one read-only method, which simplifies support for new
* data-access strategies.
*
* @see org.springframework.security.authentication.dao.DaoAuthenticationProvider
* @see UserDetails
*
* @author Ben Alex
*/
public interface UserDetailsService {
/**
* Locates the user based on the username. In the actual implementation, the search
* may possibly be case sensitive, or case insensitive depending on how the
* implementation instance is configured. In this case, the <code>UserDetails</code>
* object that comes back may have a username that is of a different case than what
* was actually requested..
*
* @param username the username identifying the user whose data is required.
*
* @return a fully populated user record (never <code>null</code>)
*
* @throws UsernameNotFoundException if the user could not be found or the user has no
* GrantedAuthority
*/
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
该接口位于org.springframework.security.core.userdetails,
主要作用:加载用户信息的核心接口;被用于DaoAuthenticationProvider的策略
2、AuthenticationFailureHandler、AuthenticationSuccessHandler接口
/**
* Strategy used to handle a failed authentication attempt.
* <p>
* Typical behaviour might be to redirect the user to the authentication page (in the case
* of a form login) to allow them to try again. More sophisticated logic might be
* implemented depending on the type of the exception. For example, a
* {@link CredentialsExpiredException} might cause a redirect to a web controller which
* allowed the user to change their password.
*
* @author Luke Taylor
* @since 3.0
*/
public interface AuthenticationFailureHandler {
/**
* Called when an authentication attempt fails.
* @param request the request during which the authentication attempt occurred.
* @param response the response.
* @param exception the exception which was thrown to reject the authentication
* request.
*/
void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException;
}
/**
* Strategy used to handle a successful user authentication.
* <p>
* Implementations can do whatever they want but typical behaviour would be to control the
* navigation to the subsequent destination (using a redirect or a forward). For example,
* after a user has logged in by submitting a login form, the application needs to decide
* where they should be redirected to afterwards (see
* {@link AbstractAuthenticationProcessingFilter} and subclasses). Other logic may also be
* included if required.
*
* @author Luke Taylor
* @since 3.0
*/
public interface AuthenticationSuccessHandler {
/**
* Called when a user has been successfully authenticated.
*
* @param request the request which caused the successful authentication
* @param response the response
* @param authentication the <tt>Authentication</tt> object which was created during
* the authentication process.
*/
void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication)
throws IOException, ServletException;
}
接口位于org.springframework.security.web.authentication;
AuthenticationFailureHandler 用于处理失败的身份验证尝试的策略;
AuthenticationSuccessHandler 当用户成功通过身份验证时调用。
3、WebSecurityConfigurerAdapter 类
@Order(100)
public abstract class WebSecurityConfigurerAdapter implements
WebSecurityConfigurer<WebSecurity> {
private final Log logger = LogFactory.getLog(WebSecurityConfigurerAdapter.class);
private ApplicationContext context;
private ContentNegotiationStrategy contentNegotiationStrategy = new HeaderContentNegotiationStrategy();
private ObjectPostProcessor<Object> objectPostProcessor = new ObjectPostProcessor<Object>() {
public <T> T postProcess(T object) {
throw new IllegalStateException(
ObjectPostProcessor.class.getName()
+ " is a required bean. Ensure you have used @EnableWebSecurity and @Configuration");
}
};
private AuthenticationConfiguration authenticationConfiguration;
private AuthenticationManagerBuilder authenticationBuilder;
private AuthenticationManagerBuilder localConfigureAuthenticationBldr;
private boolean disableLocalConfigureAuthenticationBldr;
private boolean authenticationManagerInitialized;
private AuthenticationManager authenticationManager;
private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
private HttpSecurity http;
private boolean disableDefaults;
}
该类位于org.springframework.security.config.annotation.web.configuration;
作用:为权限配置类,该类还实现了WebSecurityConfigurer接口;用户必须创建一个新类来继承AbstractHttpConfigurer。
我们将会用到的方法有:
1、protected void configure(AuthenticationManagerBuilder auth)
用户自定义注册权限,我们将使用我们自己的用户体系来重写。即,使用用户名+密码方式
auth.userDetailsService(customerUserDetailService).passwordEncoder(passwordEncoder());
2、protected void configure(HttpSecurity http)
配置Http权限,其中有`http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic()`配置
3、配置上面的权限认证成功、失败处理器以及登出处理器
@Bean
public AuthenticationSuccessHandler authenticationSuccessHandler(){
return new CustomAuthenticationSuccessHandler();
}
@Bean
public AuthenticationFailureHandler authenticationFailureHandler(){
return new CustomAuthenticationFailHandler();
}
@Bean
public LogoutHandler logoutHandler(){
return new CustomLogoutSuccessHandler();
}
技术实现
1、构建用户信息Service,实现userDetailsService
目的是:用系统的用户体系构建权限的控制;即,用户名+密码
@Component
public class CustomerUserDetailService implements UserDetailsService {
@Resource
private SysUserMapper sysUserMapper;
@Override
public UserDetails loadUserByUsername(String s){
SysUser sysUser = this.selectByUserName(s);
if(ObjectUtil.isNull(sysUser)){
throw new CustomAuthenticationException("用户不存在");
}
return this.getDetail(sysUser);
}
private UserDetails getDetail(SysUser sysUser){
return new CustomUserDetailsUser(sysUser.getId(), sysUser.getUsername(), sysUser.getPassword(), Lists.newArrayList());
}
public UserDetails loadUserByUserId(Long id){
SysUser sysUser = this.selectById(id);
if(ObjectUtil.isNull(sysUser)){
throw new CustomAuthenticationException("用户不存在");
}
return this.getDetail(sysUser);
}
}
2、分别构建权限认证成功处理器、失败处理器、登出处理器
用户名+密码匹配成功,我们将会登陆的用户信息进行token处理,下次客户端只需要透传token即可,不需要任何的登陆用户信息,信息更安全,可靠。
@Slf4j
@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Resource
private StringRedisTemplate stringRedisTemplate;
private ObjectMapper objectMapper = new ObjectMapper();
@SneakyThrows
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
String token;
Long userId = 0L;
if (authentication.getPrincipal() instanceof CustomUserDetailsUser) {
CustomUserDetailsUser userDetailsUser = (CustomUserDetailsUser) authentication.getPrincipal();
//用户名+时间
token = SecureUtil.md5(userDetailsUser.getUsername() + System.currentTimeMillis());
userId = userDetailsUser.getUserId();
} else {
token = SecureUtil.md5(String.valueOf(System.currentTimeMillis()));
}
stringRedisTemplate.opsForValue().set(Constants.AUTHENTICATION_TOKEN + token, token, Constants.TOKEN_EXPIRE, TimeUnit.SECONDS);
//返回前端的Token,V为用户的ID
stringRedisTemplate.opsForValue().set(token, Long.toString(userId), Constants.TOKEN_EXPIRE, TimeUnit.SECONDS);
response.setCharacterEncoding(CharsetUtil.UTF_8);
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
PrintWriter printWriter = response.getWriter();
Map<String, Object> dataMap = Maps.newLinkedHashMap();
dataMap.put(Constants.TOKEN, token);
printWriter.append(objectMapper.writeValueAsString(ResultVo.success(dataMap)));
}
}
权限认证失败处理器
@Slf4j
@Component
public class CustomAuthenticationFailHandler implements AuthenticationFailureHandler {
private ObjectMapper objectMapper = new ObjectMapper();
@SneakyThrows
@Override
public void onAuthenticationFailure(HttpServletRequest request,HttpServletResponse response, AuthenticationException exception){
response.setCharacterEncoding(CharsetUtil.UTF_8);
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
PrintWriter printWriter = response.getWriter();
printWriter.append(objectMapper.writeValueAsString(ResultVo.fail(exception.getMessage())));
}
}
3、分别构建图形验证码、token的过滤器
图形验证码过滤器
统一过滤请求的URL,如果登陆API,需要校验图形验证码。验证码校验失败,将无权限操作,交给权限认证失败处理器处理。
@Slf4j
public class AuthenticationTokenFilter extends BasicAuthenticationFilter {
private StringRedisTemplate stringRedisTemplate;
private CustomerUserDetailService customerUserDetailService;
private ObjectMapper objectMapper = new ObjectMapper();
public AuthenticationTokenFilter(AuthenticationManager authenticationManager, StringRedisTemplate template, CustomerUserDetailService customUserDetailsService) {
super(authenticationManager);
this.stringRedisTemplate = template;
this.customerUserDetailService = customUserDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String token = request.getHeader(Constants.TOKEN);
//如果存在Token、对Token进行校验-这个Token对应用户信息
if (!Strings.isNullOrEmpty(token)) {
String userId = stringRedisTemplate.opsForValue().get(token);
if (ObjectUtil.isNull(userId)) {
writer(response, "无效token");
return;
}
UserDetails userDetails = customerUserDetailService.loadUserByUserId(Long.valueOf(userId));
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
@SneakyThrows
public void writer(HttpServletResponse response, String msg) {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_OK);
response.getWriter()
.write(objectMapper.writeValueAsString(ResultVo.fail(HttpServletResponse.SC_UNAUTHORIZED, msg)));
}
token过滤器
将需要对token进行有效性的验证
@Slf4j
public class AuthenticationTokenFilter extends BasicAuthenticationFilter {
private StringRedisTemplate stringRedisTemplate;
private CustomerUserDetailService customerUserDetailService;
private ObjectMapper objectMapper = new ObjectMapper();
public AuthenticationTokenFilter(AuthenticationManager authenticationManager, StringRedisTemplate template, CustomerUserDetailService customUserDetailsService) {
super(authenticationManager);
this.stringRedisTemplate = template;
this.customerUserDetailService = customUserDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String token = request.getHeader(Constants.TOKEN);
//如果存在Token、对Token进行校验-这个Token对应用户信息
if (!Strings.isNullOrEmpty(token)) {
String userId = stringRedisTemplate.opsForValue().get(token);
if (ObjectUtil.isNull(userId)) {
writer(response, "无效token");
return;
}
UserDetails userDetails = customerUserDetailService.loadUserByUserId(Long.valueOf(userId));
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
@SneakyThrows
public void writer(HttpServletResponse response, String msg) {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_OK);
response.getWriter()
.write(objectMapper.writeValueAsString(ResultVo.fail(HttpServletResponse.SC_UNAUTHORIZED, msg)));
}
4、Google图形验证码配置类
加载图形验证码的bean
@Configuration
public class KaptchaConfig {
@Bean
public DefaultKaptcha producer() {
Properties properties = new Properties();
properties.put("kaptcha.border", "no");
properties.put("kaptcha.textproducer.font.color", "black");
properties.put("kaptcha.textproducer.char.space", "5");
Config config = new Config(properties);
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
5、案例实现
新建登陆API、验证码获取API
注入图形验证码的bean,写图片验证码
登陆接口无需任何操作,已经在权限认证成功的时候,生成了对应的token
@RestController
@Slf4j
public class LoginController {
@Resource
private Producer producer;
@Resource
private StringRedisTemplate stringRedisTemplate;
@SneakyThrows
@RequestMapping("/sys/code/{randomStr}")
public void captcha(@PathVariable("randomStr") String randomStr, HttpServletResponse response) {
response.setHeader("Cache-Control", "no-store, no-cache");
response.setContentType("image/jpeg");
String text = producer.createText();
log.info("【验证码生成成功】randomStr:{},captcha:{}", randomStr, text);
BufferedImage image = producer.createImage(text);
String redisKey = Constants.IMG_NUMBER_CODE_KEY + randomStr;
stringRedisTemplate.opsForValue().set(redisKey, text, Constants.TOKEN_EXPIRE, TimeUnit.SECONDS);
ServletOutputStream out = response.getOutputStream();
ImageIO.write(image, "jpg", out);
IOUtils.closeQuietly(out);
}
@PostMapping("/token/login")
@OperationLog(value = "用户登陆",type = LogOperationEnum.OTHER)
public ResultVo<?> login() {
return ResultVo.success();
}
}
用户管理API
如果没有AuthIgnore方法注解,则都需要开启token验证。
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private SysUserService sysUserService;
@AuthIgnore
@OperationLog(value = "新增用户",type = LogOperationEnum.ADD)
@PostMapping("/add")
public ResultVo<Integer> register(@RequestBody SysUser vo){
return ResultVo.success(sysUserService.add(vo));
}
@GetMapping("/info")
@SysLog(value = "用户基本信息")
public ResultVo<SysUser> info(){
return ResultVo.success(sysUserService.info());
}
}
完。
猜你喜欢
- 2024-09-29 Spring Security 自定义登录过程(非前后端分离版本)
- 2024-09-29 10分钟上手SpringSecurity 框架(3)
- 2024-09-29 SpringBoot 实现自动登录时的安全风险控制
- 2024-09-29 springboot+security框架整合 springboot security详解
- 2024-09-29 时序图说明JWT用户认证及接口鉴权的细节
- 2024-09-29 Spring Security 整合OAuth2 springsecurity整合oauth2+jwt+vue
- 2024-09-29 有关springboot + spring security的思考
- 2024-09-29 SpringSecurity之自定义用户权限信息的存取
- 2024-09-29 你还不了解SpringSecurity吗?快来看看SpringSecurity实战总结
- 2024-09-29 SpringSecurity和JWT实现认证和授权
你 发表评论:
欢迎- 最近发表
- 标签列表
-
- oraclesql优化 (66)
- 类的加载机制 (75)
- feignclient (62)
- 一致性hash算法 (71)
- dockfile (66)
- 锁机制 (57)
- javaresponse (60)
- 查看hive版本 (59)
- phpworkerman (57)
- spark算子 (58)
- vue双向绑定的原理 (68)
- springbootget请求 (58)
- docker网络三种模式 (67)
- spring控制反转 (71)
- data:image/jpeg (69)
- base64 (69)
- java分页 (64)
- kibanadocker (60)
- qabstracttablemodel (62)
- java生成pdf文件 (69)
- deletelater (62)
- com.aspose.words (58)
- android.mk (62)
- qopengl (73)
- epoch_millis (61)
本文暂时没有评论,来添加一个吧(●'◡'●)