解决目标
在单体架构下,同时容下客户端和管理端,通俗点就是一个项目单服务,同时实现页面A(用户的登录)与页面B(管理员登录),跳转到对应不同的操作端。当然两种身份的用户他不能随意进入其他端,所以需要两种不同的令牌生成和验证。用户表拆分为两个:一个normal_user表,一个super_user表,而不是简简单一个表加权限字段。
初期我是搭2个项目,但是存在部分交互,比如用户审核,商品审核等等功能。然后管理端数据库,用户端数据库,共享数据库3种,感觉这样分开系统,数据可以更好的后期拓展。但是没啥设计经验,设计的不太好,而且在交互上采用传统http调用,感觉这种调用感觉不行,感觉没法处理抛出来的异常,后续暂时没有寻找合适的解决方案(RPC?),感觉微服务openfegin不错,但是微服务比较吃服务器性能,最终就搞个单体的。感觉有更好的方式,麻烦大佬们留言,这种不同单体服务之间通信,采用何种技术合适。最好能提供链接我学学,手动狗头。
思路解析
此项目只是个demo,文末贴Gitee地址,供大家参考,代码比较粗糙,采用 JDK1.8,使用SpringBoot + SpringSecurity,数据库用mysql 和 缓存Redis,都是老面孔了。如果熟悉SpringSecurity的认证流程,可以直接跳过这个,这个单纯算自我提醒的笔记怕以后忘了
首先,了解下SpringSecurity,认证流程如下:
根据这个流程,并且结合源码了解,我们在默认调用登录时,会注入这个AuthenticationManager的authenticate的方法,其中轮询所有AuthticationProvider并调用supproets()方法判断是否支持此Authentication类,合适后调用Authenticate()方法进行认证。演示源码采用AbstractUserDetailsAuthenticationProvider这个类的
根据他源码我们得出结论1:我写入多个继承于 AuthenticationProvider 类的子类,它可以自动选择对应的子类进行登录逻辑处理,而不需要人为的指定那个方式。 具体查看源码:
从中得知,他是根据AuthenticationToken的类型进行判断,那么基于先前的结论1,得出同时写子类并继承 AbstractAuthenticationToken 与 AuthentiocationProvider即可进行自由选择不同的登录方式 如普通用户登录,需要写NormalAuthenticationToken 与 NormalAuthenticationProvider两个类,会进入普通用户登录逻辑处理。而管理员则对应写其他两个,进入管理员登录逻辑。在登录时或者拦截器处理时候则需要生成对应的AuthenticationToken即可,这个在后面代码具体实现查看。
我们继续往下看,返回AbstractUserDetailsAuthenticationProvider中,其中认证方法中,两个个方法:一个是retrieveUser(),以及additionalAuthenticationChecks()。 前者就是调用UserDetailService从数据库中获取用户数据,后者对加密数据进行处理。
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
String username = determineUsername(authentication);
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException ex) {
this.logger.debug("Failed to find user '" + username + "'");
if (!this.hideUserNotFoundExceptions) {
throw ex;
}
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
try {
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException ex) {
if (!cacheWasUsed) {
throw ex;
}
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
this.postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return createSuccessAuthentication(principalToReturn, authentication, user);
}
retriverUser代码:
additionalAuthenticationChecks代码:
对源码看了下认证流程,得出重写Provider,Token以及userDetailService 即可实现多种方式登录,然后再加上一个拦截器可以做到用户登录,访问分离。
具体代码
首先两个UserDetailsService逻辑:
@Component
public class AdminDetailsService implements UserDetailsService {
@Autowired
private SuperUserService superUserService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.err.println("进入了管理员验证功能!!-------------------------------------");
// 通过用户名从数据库获取用户信息
SuperUser userDTO = superUserService.getOne(new QueryWrapper<SuperUser>().eq("login_name",username));
// 用户不存在
if (userDTO == null) {
throw new AccountExpiredException(ErrorConstants.LOGIN_ERROR_NOTFOUND);
}
// 权限集合
List<GrantedAuthority> authorities = new ArrayList<>();
return new User(
userDTO.getLoginName (),
userDTO.getPassword(),
authorities
);
}
}
@Component
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private NormalUserService normalUserService;
@Override
public User loadUserByUsername(String username) throws UsernameNotFoundException {
System.err.println("进入了普通验证功能!!-------------------------------------");
// 通过用户名从数据库获取用户信息
NormalUser userDTO = normalUserService.getOne(new QueryWrapper<NormalUser>().eq("login_name",username));
// 用户不存在
if (userDTO == null) {
throw new AccountExpiredException (ErrorConstants.LOGIN_ERROR_NOTFOUND);
}
// 权限集合
List <GrantedAuthority> authorities = new ArrayList <> ();
return new User (
userDTO.getLoginName (),
userDTO.getPassword(),
authorities
);
}
}
其次是对应的authenticationToken 这个无脑CV原有的Token 改个名字就好了
public class NormalAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final Object principal;
private Object credentials;
/**
* This constructor can be safely used by any code that wishes to create a
* <code>UsernamePasswordAuthenticationToken</code>, as the {@link #isAuthenticated()}
* will return <code>false</code>.
*
*/
public NormalAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
/**
* This constructor should only be used by <code>AuthenticationManager</code> or
* <code>AuthenticationProvider</code> implementations that are satisfied with
* producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
* authentication token.
* @param principal
* @param credentials
* @param authorities
*/
public NormalAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true); // must use super, as we override
}
@Override
public Object getCredentials() {
return this.credentials;
}
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
Assert.isTrue(!isAuthenticated,
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
this.credentials = null;
}
}
public class SuperAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final Object principal;
private Object credentials;
/**
* This constructor can be safely used by any code that wishes to create a
* <code>UsernamePasswordAuthenticationToken</code>, as the {@link #isAuthenticated()}
* will return <code>false</code>.
*
*/
public SuperAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
/**
* This constructor should only be used by <code>AuthenticationManager</code> or
* <code>AuthenticationProvider</code> implementations that are satisfied with
* producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
* authentication token.
* @param principal
* @param credentials
* @param authorities
*/
public SuperAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true); // must use super, as we override
}
@Override
public Object getCredentials() {
return this.credentials;
}
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
Assert.isTrue(!isAuthenticated,
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
this.credentials = null;
}
}
接着是对应的TokenProvider
@Component
@Slf4j
public class NormalAuthenticationProvider implements AuthenticationProvider {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private CustomUserDetailsService userDetailsService;
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
String password =(String) authentication.getCredentials();
try {
UserDetails loadedUser = userDetailsService.loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
Collection<? extends GrantedAuthority> authorities = loadedUser.getAuthorities();
if (!passwordEncoder.matches(password, loadedUser.getPassword())) {
throw new UsernameNotFoundException("账号密码错误");
}
return new NormalAuthenticationToken(loadedUser,password,authorities);
}
catch (UsernameNotFoundException ex) {
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
@Override
public boolean supports(Class<?> authentication) {
return (NormalAuthenticationToken.class).isAssignableFrom(authentication);
}
}
@Component
@Slf4j
public class SuperAuthenticationProvider implements AuthenticationProvider {
@Autowired
private AdminDetailsService service;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
String password =(String) authentication.getCredentials();
try {
UserDetails loadedUser = service.loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
Collection<? extends GrantedAuthority> authorities = loadedUser.getAuthorities();
if (!passwordEncoder.matches(password, loadedUser.getPassword())) {
throw new UsernameNotFoundException("账号密码错误");
}
return new SuperAuthenticationToken(loadedUser,password,authorities);
}
catch (UsernameNotFoundException ex) {
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
@Override
public boolean supports(Class<?> authentication) {
return (SuperAuthenticationToken.class).isAssignableFrom(authentication);
}
}
以上是基础的类,我这边两种登陆方式,所以只需要两个,如果多种需要多个对应的创建。
以下是SpringSecuirty的配置,正常配置,主要是配置AuthenticationManager的时候稍有区别
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启方法级安全验证
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserAuthenticationEntryPointHandler userAuthenticationEntryPointHandler;
@Autowired
private JwtAccessDeniedHandler accessDeniedHandler;
@Autowired
private JwtFilter jwtFilter;
@Autowired
private NormalAuthenticationProvider normalAuthenticationProvider;
@Autowired
private SuperAuthenticationProvider superAuthenticationProvider;
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//不进行权限验证的请求或资源(从配置文件中读取)
.antMatchers("/login").permitAll()
.antMatchers("/adminLogin").permitAll()
//其他的需要登陆后才能访问
.anyRequest().authenticated()
.and()
//配置未登录自定义处理类
.httpBasic().authenticationEntryPoint(userAuthenticationEntryPointHandler)
.and()
//配置登录地址
.formLogin()
.loginProcessingUrl("/login/userLogin")
.and()
//配置没有权限自定义处理类
.exceptionHandling().accessDeniedHandler(accessDeniedHandler)
.and()
// 开启跨域
.cors()
.and()
// 取消跨站请求伪造防护
.csrf().disable();
// 基于Token不需要session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// 禁用缓存
http.headers().cacheControl();
// 添加JWT过滤器
http.addFilterBefore(jwtFilter,UsernamePasswordAuthenticationFilter.class);
}
@Override
@Bean
protected AuthenticationManager authenticationManager() throws Exception {
AuthenticationManager auth = new ProviderManager(Arrays.asList(superAuthenticationProvider,normalAuthenticationProvider));
return auth;
// return super.authenticationManager();
}
}
其次是拦截器的实现,采用正则表达式拦截请求API并且进行认证,我这边处理验证是否有token,其他直接甩给SpringSecurity的过滤器去做处理也就是AuthenticationEntryPoint类处理,导致有个问题:用户无论是登错系统,或者密码账号错误都会报同一个错误类型,所以我的登陆错系统再过滤器处理了。
@Component
public class JwtFilter extends OncePerRequestFilter {
@Autowired
JwtTokenUtil jwtTokenUtil;
private static Pattern patternNormal = Pattern.compile("^/normal/.*$");
private static Pattern patternAdmin = Pattern.compile("^/admin/.*$");
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
String token = httpServletRequest.getHeader("token");
// 没有token,去走登录流程
if (StringUtils.isBlank(token)) {
filterChain.doFilter(httpServletRequest, httpServletResponse);
return;
}
String servletPath = httpServletRequest.getServletPath();
try {
if (patternNormal.matcher(servletPath).matches()) {
if (JwtTokenUtil.validateToken(token)) {
Authentication authentication = JwtTokenUtil.getNormalAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
} else if (patternAdmin.matcher(servletPath).matches()) {
if (JwtTokenUtil.validateAdminToken(token)) {
Authentication authentication = JwtTokenUtil.getAdminAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
}catch (SignatureVerificationException e){
String errMsg = ErrorConstants.LOGIN_ERROR_SYSTEM;
httpServletResponse.setStatus( HttpStatus.UNAUTHORIZED.value () );
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json; charset=utf-8");
PrintWriter printWriter = httpServletResponse.getWriter();
printWriter.write(JSON.toJSONString(Result.error(errMsg)));
printWriter.flush();
}
}
public static void main(String[] args) {
Pattern pattern = Pattern.compile("^/normal/.*$");
String a ="/normal/visit";
Matcher matcher = pattern.matcher(a);
System.out.println(matcher.matches());
}
}
/**
* 用户登录失败处理
*/
@Slf4j
@Component
public class UserAuthenticationEntryPointHandler implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
String errMsg;
String token = request.getHeader("token");
if( StringUtils.isEmpty ( token ) ){ // 没有token抛出的异常
errMsg = StringUtils.isEmpty(authException.getMessage()) ? ErrorConstants.LOGIN_ERROR_NOT_LOGIN_IN : authException.getMessage();
response.setStatus( HttpStatus.UNAUTHORIZED.value () );
}
else {
errMsg = ErrorConstants.LOGIN_ERROR_TIMEOUT;
response.setStatus( HttpStatus.REQUEST_TIMEOUT.value () );
}
log.error (errMsg);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
PrintWriter printWriter = response.getWriter();
printWriter.write(JSON.toJSONString(Result.error(errMsg)));
printWriter.flush();
}
}
作者:用户4108570294675
链接:https://juejin.cn/post/7292984695992959003
没有回复内容