Security + JWT 实现基于Token自定义登录参数验证
简介
通俗易懂的说就是用户名+密码进行登录,后端服务器验证用户名和密码是否正确,获取用户权限等,然后生成token返回,下次调用接口时在请求头添加token,后端通过解析token来完成用户登录和权限验证
框架核心组件
1. AuthenticationManager:用户认证的管理类,所有的认证请求(比如login)都会通过提交一个token给AuthenticationManager的authenticate()方法来实现。具体校验动作会由AuthenticationManager将请求转发给具体的实现类来做(文章中自定义登录采用此处的实现类来具体实现)。
2. AuthenticationProvider,:认证的具体实现类,一个provider是一种认证方式的实现,比如提交的用户名密码我是通过和DB中查出的user记录做比对实现的,那就有一个DaoProvider。
3. UserDetailService:获取用户信息的接口spring-security抽象成UserDetailService,获取用户具体信息,也可以通过这个servcieImpl实现类具体实现。
4. AuthenticationToken,:所有提交给AuthenticationManager的认证请求都会被封装成一个Token的实现。
5. SecurityContext:当用户通过认证之后,就会为这个用户生成一个唯一的SecurityContext,里面包含用户的认证信息Authentication。
二、使用步骤
1.pom
<!-- spring security 安全认证 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2.自定义实现类
由于系统中包含了用户名+密码/手机号+验证码登录方式,所以我们需要自己实现登录验证。此篇文章将通过AuthenticationManager用户认证管理类来实现。
- SecurityConfig配置
/**
* 解决 无法直接注入 AuthenticationManager
*
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception
{
return new MyAuthenticationManager();
}
/**
* anyRequest | 匹配所有请求路径
* access | SpringEl表达式结果为true时可以访问
* anonymous | 匿名可以访问
* denyAll | 用户不能访问
* fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录)
* hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问
* hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问
* hasAuthority | 如果有参数,参数表示权限,则其权限可以访问
* hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
* hasRole | 如果有参数,参数表示角色,则其角色可以访问
* permitAll | 用户可以任意访问
* rememberMe | 允许通过remember-me登录的用户访问
* authenticated | 用户登录后可访问
*/
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception
{
httpSecurity
// CSRF禁用,因为不使用session
.csrf().disable()
// 认证失败处理类
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 过滤请求
.authorizeRequests()
// 对于登录login 验证码captchaImage 允许匿名访问
.antMatchers("/login", "/captchaImage").anonymous()
.antMatchers("/**/**/login").anonymous()
.antMatchers("/**/**/register").anonymous()
.antMatchers("/**/**/smsCode").anonymous()
.antMatchers(
HttpMethod.GET,
"/*.html",
"/**/*.html",
"/**/*.css",
"/**/*.js"
).permitAll()
.antMatchers("/profile/**").anonymous()
.antMatchers("/common/download**").anonymous()
.antMatchers("/common/download/resource**").anonymous()
.antMatchers("/swagger-ui.html").anonymous()
.antMatchers("/swagger-resources/**").anonymous()
.antMatchers("/webjars/**").anonymous()
.antMatchers("/*/api-docs").anonymous()
.antMatchers("/druid/**").anonymous()
// Spring Boot Admin Server 的安全配置
.antMatchers(adminServerProperties.getContextPath()).anonymous()
.antMatchers(adminServerProperties.getContextPath() + "/**").anonymous()
// Spring Boot Actuator 的安全配置
.antMatchers("/actuator").anonymous()
.antMatchers("/actuator/**").anonymous()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated()
.and()
.headers().frameOptions().disable();
httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
// 添加JWT filter
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 添加CORS filter
httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
}
/**
* 强散列哈希加密实现
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder()
{
return new BCryptPasswordEncoder();
}
/**
* 身份认证接口
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
{
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
此处返回MyAuthenticationManager为我们自定义的认证类。配置认证成功和失败等接口。
通过配置authenticationManagerBean这个方法,下边configure身份认证接口将不起作用。
- MyAuthenticationManager 自定义认证类
public class MyAuthenticationManager implements AuthenticationManager, AuthenticationProvider {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private DoctorDetailsService doctorDetailsService;
@Autowired
private MemberDetailsService memberDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private SysPermissionService permissionService;
@Autowired
private IWbDoctorService wbDoctorService;
private final String webType = "web";
private final String doctorType = "doctor";
private final String memberType = "member";
private static final Logger log = LoggerFactory.getLogger(MyAuthenticationManager.class);
public MyAuthenticationManager() {
System.out.println("自定义用戶验证");
}
/**
* @description 通过查看结构树可以看到,我们可以使用UsernamePasswordAuthenticationToken这个来实现用户的登录控制
* UsernamePasswordAuthenticationToken 用户名密码的令牌
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
return myAuthenticate(authentication);
}
@Override
public boolean supports(Class<?> aClass) {
return aClass.equals(UsernamePasswordAuthenticationToken.class);
}
public Authentication myAuthenticate(Authentication authentication) throws AuthenticationException {
// 简化上面的操作
UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;
// 或者 token.getPrincipal();获取用户名
String loginName = token.getName();
//LoginData为我们自定义的类,通过UsernamePasswordAuthenticationToken调用token.setDetails(data);进去的,除了用户名和密码,其它手机号验证码登录类型等都放在此处。
LoginData data = (LoginData) token.getDetails();
//userDetailsService框架中存在的接口,我们写个实现类,让他执行即可,可以获取到登录信息
UserDetails userDetails = userDetailsService.loadUserByUsername(loginName);
String presentedPassword = authentication.getCredentials().toString();
//框架底层密码验证也是这么匹配的,如果不匹配直接抛出一个密码错误的异常
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
log.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException("AbstractUserDetailsAuthenticationProvider.badCredentials");
}
return new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword());
}
}
-
UserDetailsService接口
代码中显示为jar中存在的接口,所以我们去写个实现类就可以 -
UserDetailsServiceImpl
@Service
public class UserDetailsServiceImpl implements UserDetailsService
{
private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);
@Autowired
private ISysUserService userService;
@Autowired
private SysPermissionService permissionService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
SysUser user = userService.selectUserByUserName(username);
if (Validator.isNull(user))
{
log.info("登录用户:{} 不存在.", username);
throw new UsernameNotFoundException("登录用户:" + username + " 不存在");
}
else if (UserStatus.DELETED.getCode().equals(user.getDelFlag()))
{
log.info("登录用户:{} 已被删除.", username);
throw new BaseException("对不起,您的账号:" + username + " 已被删除");
}
else if (UserStatus.DISABLE.getCode().equals(user.getStatus()))
{
log.info("登录用户:{} 已被停用.", username);
throw new BaseException("对不起,您的账号:" + username + " 已停用");
}
return createLoginUser(user);
}
public UserDetails createLoginUser(SysUser user)
{
return new LoginUser(user, permissionService.getMenuPermission(user));
}
写到这儿我们可以看见返回了一个LoginUser,这个类中包含的SysUser为刚刚调用数据查询出来的,此处做了一个账号状态校验,也可以添加其它校验逻辑。
然后回到我们前边自定义认证管理类中,可以校验登录密码是否正确,或者通过UsernamePasswordAuthenticationToken中
LoginData data = (LoginData) token.getDetails();
可以获取到手机号密码进行账号验证,只需要加上短信验证机制就可以,因为账号状态等我们已经校验完成了,当用此方式时,返回代码如下:
return new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword());
userDetails中如果是自定义的用户类,那么SysUser对象需要传入userId,userName,password,status最基础字段。因为登录为两种情况,我们没有做过多的改动,用了同一个类来生成token,如果需要区分的话,可以在生成token时根据类型去获取不同的用户类来生成。
到此处登录基本上算是完成了,因为没有写过多的自定义类。
最后再看一眼登录实现。
5. loginService
Authentication authentication = null;
try
{
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
LoginData data = new LoginData();
data.setCode(code);
data.setUuid(uuid);
data.setUserType(userType);
data.setIsFlag(isFlag);
token.setDetails(data);
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
authentication = authenticationManager.authenticate(token);
}
catch (Exception e)
{
if (e instanceof BadCredentialsException)
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
throw new UserPasswordNotMatchException();
}
else
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
throw new CustomException(e.getMessage());
}
}
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
// 生成token
return tokenService.createToken(loginUser);
登录生成token是用LoginUser 上边已经说过了,不同类型可以单独区分生成token。
基本算是比较详细了,下面链接只包含登录认证部分,不包含其它
部分代码