<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>org.springframework.securitygroupId>
<artifactId>spring-security-testartifactId>
<scope>testscope>
dependency>
用户认证服务类需要复写UserDetailsService
中的UserDetails
-> loadUserByUsername(String username)
方法
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private final MageService service; // 用户信息CRUD服务类
public UserDetailsServiceImpl(MageService service) {
this.service = service;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Mage usr = service.findByName(username); // 根据username查询用户
if (usr != null) {
return new User(usr.getMageName(), usr.getMagePassword(), AuthorityUtils.NO_AUTHORITIES);
} else {
throw new UsernameNotFoundException("当前用户不存在!");
}
}
}
loadUserByUsername(String username) 方法返回的类型是UserDetails,创建UserDetails类需要传3个参数
分别是username,password,Collection extends GrantedAuthority> authorities
authorities是用来设置用户权限访问的,这个参数即使表中没有也不能不传,如果需求不需要设置权限,可以传AuthorityUtils.NO_AUTHORITIES,表示没权限设置
WebSecurityConfigurerAdapter
@Configuration // 声明配置类
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
/**
* 设置加密方式
*/
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 用户认证
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
前后端不分离,安全认证要做的事是拦截请求,根据配置的情况,跳转到指定页面
前后端分离,安全认证要做的事是拦截请求,根据配置的情况,返回json格式的提示信息给前端,让前端做跳转,后端就不再需要写跳转的代码了
前后端不分离,使用.successForwardUrl() 或者.defaultSuccessUrl() 实现登录成功后的页面跳转
前后端分离,使用.successHandler()实现登录成功后向前端发送json格式的回馈信息
.formLogin()
.loginProcessingUrl("/doLogin")
.permitAll()
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
Map<String, Object> map = new HashMap<>();
map.put("msg", "登录成功!");
map.put("principal", authentication.getPrincipal());
resp.setContentType("application/json:charset=utf-8");
PrintWriter out = resp.getWriter();
// 对象转json传输给前端
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
}
})
这里需要重点讲讲 .loginPage()
和 .loginProcessingUrl()
这两者的区别
.loginPage(/xxx)
: 在没有登录的情况下,用户访问需要登录才能访问的页面时,会自动跳转到 /xxx这个地址,这个地址可以是指定一个xxx.html页面,也可以是一个跳转页面的接口
.loginProcessingUrl()
:前端发送登录表单的目标地址,也就是 action = ""中的值
对于前后端分离的项目,后端是不需要处理跳转问题的,所以一般也不会用到.loginPage(/xxx)
authentication.getPrincipal()
:获取的是登录用户的基本信息
前后端不分离,使用.failureForwardUrl() 或者.failureUrl() 实现登录失败后的页面跳转
前后端分离,使用.failureHandler()实现登录失败后向前端发送json格式的回馈信息
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException {
Map<String, Object> map = new HashMap<>();
map.put("msg", "登录失败!");
resp.setContentType("application/json:charset=utf-8");
PrintWriter out = resp.getWriter();
// 对象转json传输给前端
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
}
})
.logout()
.logoutUrl("/logout")
.permitAll()
.logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
Map<String, Object> map = new HashMap<>();
map.put("state", 200);
map.put("msg", "注销成功!");
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
// 对象转json传输给前端
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
}
})
如果用户访问需要登录才能访问的页面,后端将会返回提示信息给前端
.exceptionHandling()// 异常抛出
.authenticationEntryPoint(new AuthenticationEntryPoint() {
@Override
public void commence(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException {
Map<String, Object> map = new HashMap<>();
map.put("state", 403);
map.put("msg", "没有访问权限,请先登录!");
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
// 对象转json传输给前端
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
}
})
到这里,前后端分离的Spring Security的配置就完成了,但是上面的方式只支持前端以表单的形式发送登录请求,如果想用json格式发送登录请求,上面的配置是不支持的,下面提供的是既支持表单也支持json格式的请求
我们先来看看,Spring Security默认支持表单请求登录的源码
类名:UsernamePasswordAuthenticationFilter
方法:
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
// 以Key-value的形式获取用户名和密码
String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
我们可以看到,Spring Security是以key-value的形式获取用户名和密码的,也就是表单的形式,如果前端传输的是json的格式,Spring Security是获取不到用户名和密码的,所以我们只需要修改Spring Security获取用户名和密码的方式就可以了。
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
// 不是post请求抛出异常
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
// 判断请求是否是json格式,如果不是直接调用父类
if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) {
// 把request的json数据转换为Map
Map<String, String> loginData = new HashMap<>();
try {
loginData = new ObjectMapper().readValue(request.getInputStream(), Map.class);
} catch (IOException e) {
e.printStackTrace();
}
// 调用父类的getParameter() 方法获取key值
String username = loginData.get(this.getUsernameParameter());
String password = loginData.get(this.getPasswordParameter());
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
} else {
return super.attemptAuthentication(request, response);
}
}
}
根据请求的类型(表单还是json)去执行相应获取用户名和密码的代码,如果是表单,我们直接调用父类方法super.attemptAuthentication(request, response);
就可以了,否则获取json数据中的用户名和密码
/**
* 自定义 UsernamePasswordAuthenticationFilter 过滤器
*/
@Bean
LoginFilter loginFilter() throws Exception {
LoginFilter loginFilter = new LoginFilter();
// 前端的登录请求地址
loginFilter.setFilterProcessesUrl("/doLogin");
// 登录成功后返回给前端的json数据
loginFilter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
Map<String, Object> map = new HashMap<>();
map.put("state", 200);
map.put("msg", "登录成功!");
map.put("principal", authentication.getPrincipal());
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
// 对象转json传输给前端
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
}
});
// 登录失败后返回给前端的json数据
loginFilter.setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException {
Map<String, Object> map = new HashMap<>();
map.put("state", 403);
map.put("msg", "登录失败!");
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
// 对象转json传输给前端
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
}
});
loginFilter.setAuthenticationManager(authenticationManagerBean());
return loginFilter;
}
登录成功和登录失败的反馈信息在LoginFilter实现过后,前面在.formLogin()
中实现的登录成功和登录失败的代码就不需要了,可以删除。
configure(HttpSecurity http)
中添加LoginFilter过滤器// 把默认的UsernamePasswordAuthenticationFilter 过滤器替换成自定义过滤器loginFilter
http.addFilterAfter(loginFilter(), UsernamePasswordAuthenticationFilter.class);