spring Security前后端分离返回token信息
后端项目地址:https://gitee.com/kitter/vd-mall
前端项目地址:https://gitee.com/kitter/vd-mall-web
添加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<String> 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中。