spring-Security前后端分离返回token信息

105 阅读4分钟

后端项目地址:gitee.com/kitter/vd-m…

前端项目地址:gitee.com/kitter/vd-m…

添加Security配置类

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@PropertySource("classpath:security-config.properties")
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Value("${security.ignore.resource}")
    private String[] securityIgnoreResource;
 
    @Value("${security.ignore.api}")
    private String[] securityIgnoreApi;
 
    @Value("${security.login.url}")
    private String loginApi;
 
    @Value("${security.logout.url}")
    private String logoutApi;
 
    @Value("${security.login.username.key:username}")
    private String usernameKey;
 
    @Value("${security.login.password.key:password}")
    private String passwordKey;
 
    @Autowired
    UserDetailService userDetailService;
 
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.csrf().disable()
                .authorizeRequests()
                //对于静态资源的获取允许匿名访问
                .antMatchers(HttpMethod.GET, securityIgnoreResource).permitAll()
                // 对登录注册要允许匿名访问;
                .antMatchers(securityIgnoreApi).permitAll()
                //其余请求全部需要登录后访问
                .anyRequest().authenticated()
                //这里配置的loginProcessingUrl为页面中对应表单的 action ,该请求为 post,并设置可匿名访问
                .and().formLogin().loginProcessingUrl(loginApi).permitAll()
                //这里指定的是表单中name="username"的参数作为登录用户名,name="password"的参数作为登录密码
                .usernameParameter(usernameKey).passwordParameter(passwordKey)
                //登录成功后的返回结果
                .successHandler(new AuthenticationSuccessHandlerImpl())
                //登录失败后的返回结果
                .failureHandler(new AuthenticationFailureHandlerImpl(usernameKey))
                //这里配置的logoutUrl为登出接口,并设置可匿名访问
                .and().logout().logoutUrl(logoutApi).permitAll()
                //登出后的返回结果
                .logoutSuccessHandler(new LogoutSuccessHandlerImpl())
                //这里配置的为当未登录访问受保护资源时,返回json
                .and().exceptionHandling().authenticationEntryPoint(new AuthenticationEntryPointHandler());
    }
 
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailService).passwordEncoder(passwordEncoder());
    }
 
    @Bean
    public PasswordEncoder passwordEncoder() {
        //配置密码加密,这里声明成bean,方便注册用户时直接注入
        return new BCryptPasswordEncoder();
    }
}
 

@EnableWebSecurity:开启Security,该注解中包含@Import注解,使得WebSecurityConfiguration配置类生效 @EnableGlobalMethodSecurity(prePostEnabled = true):开启访问权限 @PropertySource("classpath:security-config.properties") 这里自定义的一个properties文件,通过@PropertySource注解导入后可以用@Value 读取里面的值,我这里配置的主要是SwaggerUI静态资源的忽略 以及登入登出的api地址,由于这类配置几乎不会被修改,因此这里直接独立出来一分配置文件

security.ignore.resource=/swagger-resources/**, /v2/api-docs/**, /webjars/springfox-swagger-ui/**, /swagger-ui.html,/**/*.js
security.ignore.api=/admin/api/v1/users/register
security.login.url=/admin/api/v1/users/login
security.logout.url=/admin/api/v1/users/logout

上面我们提到过前后端分离我们希望所有的返回结果都以json的方式返回给前台。但是Spring Security 默认返回页面,这里我们特殊处理下。我们可以看到在 Spring Security 配置类中 有几个Handler,这里就是我们需要自己实现的地方:

AuthenticationSuccessHandlerImpl.java 这个类定义登录成功的返回结果

@Slf4j
public class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        //登录成功后获取当前登录用户
        UserDetail userDetail = (UserDetail) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        log.info("用户[{}]于[{}]登录成功!", userDetail.getUser().getUsername(), new Date());
        WriteResponse.write(httpServletResponse, new SuccessResponse());
    }
}

AuthenticationFailureHandlerImpl.java 这个类定义登录失败的返回结果,我们区分了登录失败的类型 

@Slf4j
public class AuthenticationFailureHandlerImpl implements AuthenticationFailureHandler {
    private String usernameKey;
 
    public AuthenticationFailureHandlerImpl(String usernameKey) {
        this.usernameKey = usernameKey;
    }
 
    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        GlobalResponseCode code;
 
        if (e instanceof BadCredentialsException || e instanceof UsernameNotFoundException) {
            code = GlobalResponseCode.USERNAME_OR_PASSWORD_ERROR;
        } else if (e instanceof LockedException) {
            code = GlobalResponseCode.ACCOUNT_LOCKED_ERROR;
        } else if (e instanceof CredentialsExpiredException) {
            code = GlobalResponseCode.CREDENTIALS_EXPIRED_ERROR;
        } else if (e instanceof AccountExpiredException) {
            code = GlobalResponseCode.ACCOUNT_EXPIRED_ERROR;
        } else if (e instanceof DisabledException) {
            code = GlobalResponseCode.ACCOUNT_DISABLED_ERROR;
        } else {
            code = GlobalResponseCode.LOGIN_FAILED_ERROR;
        }
        RestResponse response = new ErrorResponse(code);
        String username = httpServletRequest.getParameter(usernameKey);
        log.info("用户[{}][{}]登录失败,失败原因:[{}]", username, new Date(), response.getMessage());
 
        WriteResponse.write(httpServletResponse, response);
    }
}
 

LogoutSuccessHandlerImpl.java  这个类定义登出返回结果

@Slf4j
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        if (authentication != null) {
            log.info("用户[{}]于[{}]注销成功!", ((UserDetail) authentication.getPrincipal()).getUsername(), new Date());
        }
       
        WriteResponse.write(httpServletResponse, new SuccessResponse());
    }
}
 

AuthenticationEntryPointHandler.java 这里配置的为当未登录访问受保护资源时,返回json

public class AuthenticationEntryPointHandler implements AuthenticationEntryPoint {
   @Override
   public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
       WriteResponse.write(httpServletResponse,  new ErrorResponse(GlobalResponseCode.ACCESS_FORBIDDEN_ERROR));
   }
}
WriteResponse.java 将返回内容写入HttpServletResponse 

class WriteResponse {
   private static final ObjectMapper mapper = new ObjectMapper();

   static void write(HttpServletResponse httpServletResponse, RestResponse restResponse) throws IOException {
       httpServletResponse.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
       PrintWriter out = httpServletResponse.getWriter();
       out.write(mapper.writeValueAsString(restResponse));
       out.flush();
       out.close();
   }
}
 ```

到这里我们的配置已经基本完成,当然可能有人会问用户登录时是怎么连接数据库做查询的,这里就是我们接下来要说的。

首先创建我们的用户类 User.java

@Data public class User { private int id; private String username; private String password; private String nickname; private String avatar; private int sex; private boolean accountNonExpired; private boolean accountNonLocked; private boolean credentialsNonExpired; private boolean enabled; private Date updateTime; private Date createTime; }

创建 UserDetail.java 类 继承 UserDetails ,UserDtails 类是 Security 中对用户的抽象,包含用户的基本信息,以及角色信息

public class UserDetail implements UserDetails { private User user; private List roles;

public List<String> getRoles() {
    return roles;
}

public void setRoles(List<String> roles) {
    this.roles = roles;
}

public User getUser() {
    return user;
}

public void setUser(User user) {
    this.user = user;
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
    if (roles == null || roles.isEmpty()) {
        return new ArrayList<>();
    }

    List<GrantedAuthority> authorities = new ArrayList<>(roles.size());
    for (String role : roles) {
        authorities.add(new SimpleGrantedAuthority(role));
    }

    return authorities;
}

@Override
public String getPassword() {
    return user.getPassword();
}

@Override
public String getUsername() {
    return user.getUsername();
}

@Override
public boolean isAccountNonExpired() {
    return user.isAccountNonExpired();
}

@Override
public boolean isAccountNonLocked() {
    return user.isAccountNonLocked();
}

@Override
public boolean isCredentialsNonExpired() {
    return user.isCredentialsNonExpired();
}

@Override
public boolean isEnabled() {
    return user.isEnabled();
}

}


创建 UserDetailService 继承 UserDetailsService 类,根据用户名查询用户信息

@Service public class UserDetailService implements UserDetailsService { @Autowired UserDao userDao;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    UserDetail userDetail = userDao.getUserDetailsByUserName(username);
    if (userDetail == null) {
        throw new UsernameNotFoundException("Not found username:" + username);
    }
    return userDetail;
}

}

配置Security 的认证管理器,在我们前面的配置类中重写 WebSecurityConfigurerAdapter 的protected void configure(AuthenticationManagerBuilder auth) 方法即可

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

vue 实现登录功能集成
这里就不具体讲述Vue的详细流程了后面开单章进行讲述,这里看下效果吧,可以看到登录成功之后sessionid已经写入到了Cookies中。