之前都是自个实现登录拦截、用户验证,自从发现了研究springSecurity这个框架之后,爱不释手,基本上权限+验证方面的操作都可以完成,是spring中一个重量级的安全校验框架。
很多人抱怨springSecurity的api太复杂,其实不然,当你了解它的流程操作时,用起来相当顺手,搭配springboot可快速搭建一个有权限控制级别的项目。
在使用springSecurity之前,我们先来认识一下它的核心类。
Authentication
Authentication是表示用户信息的一个接口,通过它可封装一些用户登录认证的对象,用户认证成功后,又会生成一个信息更加全面,包括用户所拥有的角色等信息的Authentication对象,然后把它保存在SecurityContextHolder 所持有的 SecurityContext 中,供后续的程序进行调用,如访问权限的鉴定等。
SecurityContextHolder
根据以上所知,它是一个用来保存SecurityContext的一个类,而SecurityContext中又存储着用户信息。它含有很多静态方法,如getContext、setContext、clearContext来对SecurityContext进行操作。
AuthenticationManager 和 AuthenticationProvider
AuthenticationManager 是一个用来处理认证(Authentication)请求的接口。在其中只定义了一个方法 authenticate(),该方法只接收一个代表认证请求的 Authentication 对象作为参数,如果认证成功,则会返回一个封装了当前用户权限等信息的 Authentication 对象进行返回。
AuthenticationManager 的默认实现是 ProviderManager,它不自己处理请求,而是通过委托机制给其配置的AuthenticationProvider 列表,然后会依次使用每一个 AuthenticationProvider 进行认证,如果有一个 AuthenticationProvider 认证后的结果不为 null,则表示该 AuthenticationProvider 已经认证成功,之后的 AuthenticationProvider 将不再继续认证
UserDetailsService
认识这个接口之前我们先来看一下UserDetails接口,UserDetails中定义了用户的账户、密码、权限等信息,可通过实现该接口中的方式自行定义用户信息类。而UserDetailsService中只有一个方法loadUserByUsername(),该方法返回的便是一个UserDetails对象。这个方法主要是对web传来的用户信息进行验证操作,验证成功便返回用户所属的UserDetails信息。用户验证不通过则抛出UsernameNotFoundException异常。
现在我们来开始搭建我们的springSecurity。
首先,定义好我们的用户信息类User
@Entity
@Table(name = "SISE_USER")
public class User extends BaseEntity{
@Column(name="USER_USERNAME",length=400)
private String username;
@Column(name="USER_ACCOUNT",length=400)
private String account;
@Column(name="USER_PASSWORD",length = 400)
private String password;
@Column(name="USER_EMAIL",length = 400)
private String email;
@Column(name = "USER_PHONE",length = 400)
private String phone;
@ManyToMany(fetch = FetchType.EAGER, targetEntity = Role.class)
@JoinTable(name = "TT_USER_ROLE",joinColumns = @JoinColumn(name = "USER_ID"),inverseJoinColumns = @JoinColumn(name = "ROLE_ID"))
private Set<Role> roles = new LinkedHashSet<>();
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getAccount() {
return account;
}
public void setAccount(String account) {
this.account = account;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public Set<Role> getRoles() {
return roles;
}
public void setRoles(Set<Role> roles) {
this.roles = roles;
}
}
然后创建一个SecurityUser类来继承这个User和实现Usertails接口
public class SecurityUser extends User implements UserDetails {
private static final long serialVersionUID = 1L;
public SecurityUser(User suser) {
if (suser != null)
{
this.setsId(suser.getsId());
this.setUsername(suser.getUsername());
this.setAccount(suser.getAccount());
this.setEmail(suser.getEmail());
this.setPassword(suser.getPassword());
this.setRoles(suser.getRoles());
}
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
Set<Role> roles = super.getRoles();
if(roles != null){
for(Role role : roles){
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(role.getRoleName());
authorities.add(authority);
}
}
return authorities;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
到了这里,我们的用户信息基本定义完成。
接下来我们来实现UserDetailsService接口,我们创建一个CustomerUserService来实现它。
@Component
public class CustomUserDetailsService implements UserDetailsService{
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String account) throws UsernameNotFoundException {
User user = userService.findByAccount(account);
if(user == null){
throw new UsernameNotFoundException("Username :" + account + "not found");
}
SecurityUser securityUser = new SecurityUser(user);
return securityUser;
}
}
记得使用@Component把它注册为组件,便于spring进行实例化。
接下来就是要对springSercurity进行配置和使用,我们创建一个WebSecurityConfig继承WebSecurityConfiguConfigurerAdater
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
@Override
protected void configure(HttpSecurity http) throws Exception{
http.authorizeRequests().anyRequest().authenticated()
.and()
.formLogin().loginPage("/login_p")
.loginProcessingUrl("/login")
.usernameParameter("account")
.passwordParameter("password")
.permitAll()
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter out = httpServletResponse.getWriter();
StringBuffer sb = new StringBuffer();
sb.append("{\"status\":\"error\",\"msg\":\"");
if (e instanceof UsernameNotFoundException || e instanceof BadCredentialsException) {
sb.append("用户名或密码输入错误,登录失败!");
} else if (e instanceof DisabledException) {
sb.append("账户被禁用,登录失败,请联系管理员!");
} else {
sb.append("登录失败!");
}
sb.append("\"}");
out.write(sb.toString());
out.flush();
out.close();
}
}).successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter out = httpServletResponse.getWriter();
SecurityUser user = (SecurityUser) SecurityContextHolder.getContext()
.getAuthentication()
.getPrincipal();
Set<Menu> menus = new HashSet<>();
for(Role role : user.getRoles()){
menus.addAll(role.getMenus());
}
ArrayNode datas = JsonUtils.createArrayNode();
ObjectNode data;
data = datas.addObject();
data.put("account",user.getAccount());
data.put("userId",user.getsId());
data.put("username",user.getUsername());
ObjectMapper objectMapper = new ObjectMapper();
String s = "{\"status\":\"success\",\"msg\":" + objectMapper.writeValueAsString(datas) + "}";
out.write(s);
out.flush();
out.close();
}
}).and().logout().permitAll().and().csrf().disable();
}
@Autowired
public void globalConfigure(AuthenticationManagerBuilder auth) throws Exception{
auth.userDetailsService(customUserDetailsService())
.passwordEncoder(passwordEncoder());
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public CustomUserDetailsService customUserDetailsService(){
return new CustomUserDetailsService();
}
}
http.authorizeRequests().anyRequest().authenticated()
.and()
.formLogin().loginPage("/login_p")
.loginProcessingUrl("/login")
.usernameParameter("account")
.passwordParameter("password")
.permitAll()
loginPage方法指定登录界面,因为我们使用vue前后端分离,所以这里可以不用指定,可直接去掉
loginprocessingUrl方法设置登录接口,前端登录访问的接口,默认为/login。
usernameParameter和passwordParameter便是接口所需的参数。
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter out = httpServletResponse.getWriter(); StringBuffer sb = new StringBuffer();
sb.append("{\"status\":\"error\",\"msg\":\"");
if (e instanceof UsernameNotFoundException || e instanceof BadCredentialsException) {
sb.append("用户名或密码输入错误,登录失败!");
} else if (e instanceof DisabledException) {
sb.append("账户被禁用,登录失败,请联系管理员!");
} else {
sb.append("登录失败!");
}
sb.append("\"}");
out.write(sb.toString());
out.flush();
out.close();
}
该方法主要是登录失败之后所进行的操作
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter out = httpServletResponse.getWriter();
SecurityUser user = (SecurityUser) SecurityContextHolder.getContext()
.getAuthentication()
.getPrincipal();
Set<Menu> menus = new HashSet<>();
for(Role role : user.getRoles()){
menus.addAll(role.getMenus());
}
ArrayNode datas = JsonUtils.createArrayNode();
ObjectNode data;
data = datas.addObject();
data.put("account",user.getAccount());
data.put("userId",user.getsId());
data.put("username",user.getUsername());
ObjectMapper objectMapper = new ObjectMapper();
String s = "{\"status\":\"success\",\"msg\":" + objectMapper.writeValueAsString(datas) + "}";
out.write(s);
out.flush();
out.close();
}
})
登录成功后所进行的操作。
在登录中,我们还需要对密码进行加密,springSecurity有很多hash加密方法,在这里我们使用BCryptPasswordEncoder对密码进行加密
@Autowired
public void globalConfigure(AuthenticationManagerBuilder auth) throws Exception{
auth.userDetailsService(customUserDetailsService())
.passwordEncoder(passwordEncoder());
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
到了这一步,我们就基本完成了一个登录验证的功能,后面我会用vue+springboot完成一个拥有权限控制的系统。