1. 简介
在看本文章之前,建议去看下传统web项目表单认证的原理:SpringSecurity系列一:10 传统Web项目表单认证: UsernamePasswordAuthenticationFilter 过滤器
在前后端分离的项目中,前端会以 json 格式来传递参数,这就需要我们自定义登录过滤器链来实现。登录参数的提取是在UsernamePasswordAuthenticationFilter过滤器中完成的,如果要使用 Json 格式登录,只要模仿UsernamePasswordAuthenticationFilter过滤器自定义自己的过滤器,再将自定义的额过滤器放到UsernamePasswordAuthenticationFilter过滤器所在的位置即可。
1. 自定义过滤器使用Json数据格式认证登录
①定义一个LoginFilter 继承自UsernamePasswordAuthenticationFilter,当进行认证时,请求不再进入UsernamePasswordAuthenticationFilter,而是进入LoginFilter
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
// 1. 判断是否是post请求方式
if(!request.getMethod().equals("POST")){
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
// 2. 判断是否是json格式请求类型
if(request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)){
// 3. 从json数据中获取用户输入用户名和密码认证:{"uname":"xxx","password":"xxx"}
try {
Map<String,String> userInfo = new HashMap<>();
userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
String username = userInfo.get(getUsernameParameter());
String password = userInfo.get(getPasswordParameter());
System.out.println("username = " + username);
System.out.println("password = " + password);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username,password);
setDetails(request,authenticationToken);
return this.getAuthenticationManager().authenticate(authenticationToken);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// 4. 如果不是json格式,那么按照父类的表单登录逻辑处理
return super.attemptAuthentication(request,response);
}
}
②在SpringSecurity配置类中配置替换UsernamePasswordAuthenticationFilter:
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
@Bean
public UserDetailsService userDetailsService(){
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("admin").build());
return inMemoryUserDetailsManager;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService());
}
// 暴露自定义的authenticationManager
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Bean
public LoginFilter loginFilter() throws Exception {
LoginFilter loginFilter = new LoginFilter();
loginFilter.setFilterProcessesUrl("/doLogin");
loginFilter.setUsernameParameter("uname");
loginFilter.setPasswordParameter("passwd");
loginFilter.setAuthenticationManager(authenticationManager());
loginFilter.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler());
loginFilter.setAuthenticationFailureHandler(new MyAuthenticationFailureHandler());
return loginFilter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 开启请求的权限管理
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable();
// 将loginFilter过滤器添加到UsernamePasswordAuthenticationFilter过滤器所在的位置
http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final ObjectMapper objectMapper = new ObjectMapper() ;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
Map<String,Object> map = new HashMap<>();
map.put("msg","登录成功");
map.put("code",200);
map.put("authentication",authentication);
response.setContentType("application/json;charset=UTF-8");
String s = objectMapper.writeValueAsString(map);
response.getWriter().println(s);
}
}
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
Map<String,Object> map = new HashMap<>();
map.put("msg","登录失败:"+exception.getMessage());
map.put("code",-1);
response.setContentType("application/json;charset=UTF-8");
String s = objectMapper.writeValueAsString(map);
response.getWriter().println(s);
}
}
③访问控制器:
@Controller
public class LoginController {
@RequestMapping("/login.html")
public String login() {
return "login";
}
}
④启动项目,测试:
⑤认证成功时,默认SpringSecurity会发送一个Cookie:
访问其他请求时 http://localhost:8080/hello 需要带上Cookie,Postman默认会带上Cookie:
2. 退出登录
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
// ...
@Override
protected void configure(HttpSecurity http) throws Exception {
// 开启请求的权限管理
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
// 退出登录
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(new MyLogoutSuccessHandler())
.and()
.csrf().disable();
// 将loginFilter过滤器添加到UsernamePasswordAuthenticationFilter过滤器所在的位置
http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
Map<String,Object> map = new HashMap<>();
map.put("msg","注销登录");
map.put("code",200);
map.put("authentication",authentication);
response.setContentType("application/json;charset=UTF-8");
String s = objectMapper.writeValueAsString(map);
response.getWriter().println(s);
}
}
3. 没有认证时的异常处理
如果没有认证就访问某个需要认证的请求,会进入登录页面,但是前后端分离一般不会跳转到登录页面,而是自定义处理,因此可以配置未认证时的异常处理:
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
// ...
@Override
protected void configure(HttpSecurity http) throws Exception {
// 开启请求的权限管理
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
// 未认证时的异常处理
.exceptionHandling()
.authenticationEntryPoint(new MyAuthenticationEntryPoint())
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(new MyLogoutSuccessHandler())
.and()
.csrf().disable();
// 将loginFilter过滤器添加到UsernamePasswordAuthenticationFilter过滤器所在的位置
http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().println("请认证之后再去处理");
}
}
4. 更改认证数据源为数据库认证
①引入依赖:
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.15</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency>
②配置SpringBoot
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test?characterEncoding=UTF-8&useSSL=false&serverTimezone=Hongkong
spring.datasource.username=root
spring.datasource.password=root
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=com.hh.entity
logging.level.com.baizhi=debug
③User 实体类
@Data
public class User implements UserDetails{
private Integer id;
private String username;
private String password;
private Boolean enabled;
private Boolean accountNonExpired;
private Boolean accountNonLocked;
private Boolean credentialsNonExpired;
private List<Role> roles = new ArrayList<>();
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
roles.forEach(role -> grantedAuthorities.add(new SimpleGrantedAuthority(role.getName())));
return grantedAuthorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return accountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return accountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return credentialsNonExpired;
}
@Override
public boolean isEnabled() {
return enabled;
}
}
④Role 实体类
@Data
public class Role {
private Integer id;
private String name;
private String nameZh;
}
⑤UserDao
@Mapper
public interface UserDao {
// "根据用户名查询用户
User loadUserByUsername(String username);
// "根据用户id查询⻆色
List<Role> getRolesByUid(Integer uid);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.hh.dao.UserDao">
<select id="loadUserByUsername" resultType="com.hh.entity.User">
select * from user where username ={username}
</select>
<select id="getRolesByUid" resultType="com.hh.entity.Role">
select
r.id,
r.name,
r.name_zh nameZh
from role r, user_role ur
where r.id = ur.rid
and ur.uid ={uid}
</select>
</mapper>
⑥MyUserDetailsService
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserDao userDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userDao.loadUserByUsername(username);
if(Objects.isNull(user)){
throw new RuntimeException("用户不存在");
}
user.setRoles(userDao.getRolesByUid(user.getId()));
return user;
}
}
⑦配置SpringSecurity
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
// 更改数据源
@Autowired
private UserDetailsService userDetailsService;
// 配置数据源
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
// 暴露自定义的authenticationManager
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Bean
public LoginFilter loginFilter() throws Exception {
LoginFilter loginFilter = new LoginFilter();
loginFilter.setFilterProcessesUrl("/doLogin");
loginFilter.setUsernameParameter("uname");
loginFilter.setPasswordParameter("passwd");
loginFilter.setAuthenticationManager(authenticationManager());
loginFilter.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler());
loginFilter.setAuthenticationFailureHandler(new MyAuthenticationFailureHandler());
return loginFilter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 开启请求的权限管理
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.exceptionHandling()
.authenticationEntryPoint(new MyAuthenticationEntryPoint())
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(new MyLogoutSuccessHandler())
.and()
.csrf().disable();
// 将loginFilter过滤器添加到UsernamePasswordAuthenticationFilter过滤器所在的位置
http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
⑧启动项目测试: