一、HttpBasic模式登录认证
SpringSecurity 自带一种基础认证模式
实现方式:创建WebSecurityConfig配置类
/**
* Spring Securtiy配置类
*/
@Configuration //配置类
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
}
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//开启httpBasci模式登录认证
http.httpBasic()
//每个模块配置使用and结尾
.and()
//配置路径拦截,表明路径访问所对应的权限,角色,认证信息
.authorizeRequests()
.anyRequest()
//所有请求都需要登录认证才能访问
.authenticated();
}
}
二、 默认formLogin表单模式
注释掉上面的配置类,通过applicaton.yml配置用户名与密码
spring:
security:
user:
name: tom
password: tom
三、 自定义formLogin表单模式
继承WebSecurityConfigurerAdapter类,实现它的三个configure方法
//@Configuration
@EnableWebSecurity //涵盖了 @Configuration 注解
//@EnableGlobalMethodSecurity(jsr250Enabled = true)
//@EnableGlobalMethodSecurity(securedEnabled = true)
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启Security注解鉴权的功能
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private UserServiceImpl userServiceImpl;
@Autowired
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
@Autowired
private MyAuthenticationEntryPoint myAuthenticationEntryPoint;
@Autowired
private MyAccessDeniedHandler myAccessDeniedHandler;
@Autowired
private JwtTokenAuthenticationFilter jwtTokenAuthenticationFilter;
@Autowired
private MyLogoutSuccessHandler myLogoutSuccessHandler;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//在内存中配置用户名和密码
// auth.inMemoryAuthentication().withUser("tom")
// .password(passwordEncoder.encode("123")).roles();
// auth.inMemoryAuthentication().withUser("admin")
// .password(passwordEncoder.encode("admin")).roles();
//从指定的数据库读取账号密码
// auth.userDetailsService(userServiceImpl).passwordEncoder(passwordEncoder);
//指定认证管理器
auth.authenticationProvider(getDaoAuthenticationProvider());
}
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//开启httpBasic认证
// http.httpBasic()
// //每个模块配置使用and结尾
// .and()
// //配置路径拦截,表明路径访问所对应的权限,角色,认证信息
// .authorizeRequests()
// .anyRequest()
// //所有请求都需要登录认证才能访问
// .authenticated();
//http.httpBasic().and().authorizeRequests().anyRequest().authenticated(); //关闭httpBasic认证
//开启自定义formLogin表单认证
//需要放行的url在这里配置,必须要放行/login和/login.html,不然会报错
http.authorizeRequests().antMatchers("/login", "/login.html")
.permitAll().anyRequest().authenticated().and().
// 设置登陆页、登录表单form中action的地址,也就是处理认证请求的路径
formLogin().loginPage("/login.html").loginProcessingUrl("/login")
//登录表单form中密码输入框input的name名,不修改的话默认是password
.usernameParameter("username").passwordParameter("password")
//登录认证成功后默认转跳的路径
// .defaultSuccessUrl("/home")
//登陆成功后不跳转,返回json数据
.successHandler(myAuthenticationSuccessHandler)
//登录认证失败后,被放行的请求
//注意,此请求不能为error,因为已经被springSecurity定义
// .failureUrl("/gotoError1").permitAll();
//登录认证失败后,不跳转,返回json数据
.failureHandler(myAuthenticationFailureHandler)
.and()
.exceptionHandling()
.authenticationEntryPoint(myAuthenticationEntryPoint)
.accessDeniedHandler(myAccessDeniedHandler)
.and().logout().logoutSuccessHandler(myLogoutSuccessHandler);
//将自定义的JwtTokenAuthenticationFilter插入到过滤器链的指定过滤器前面
http.addFilterBefore(jwtTokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
//关闭CSRF跨域
http.csrf().disable();
//关闭session最严格的策略
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
/**
* 重新定制DaoAuthenticationProvider
* @return
*/
@Bean
public DaoAuthenticationProvider getDaoAuthenticationProvider(){
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
//设置用户未找到的异常不隐藏
provider.setHideUserNotFoundExceptions(false);
//设置认证管理器使用userServiceImpl对象
provider.setUserDetailsService(userServiceImpl);
//设置认证管理器使用的密码管理对象
provider.setPasswordEncoder(passwordEncoder);
return provider;
}
}
1. 配置需要直接放行的url,处理认证请求的路径
protected void configure(HttpSecurity http) throws Exception {
//开启自定义表单模式登录认证
//需要放行的url在这里配置,必须要放行/login和/login.html,不然会报错
http.authorizeRequests().antMatchers("/login", "/login.html")
.permitAll().anyRequest().authenticated()
.and().
// 设置登陆页、登录表单form中action的地址,也就是处理认证请求的路径
formLogin().loginPage("/login.html").loginProcessingUrl("/login")
//登录表单form中密码输入框input的name名,不修改的话默认是password
.usernameParameter("username").passwordParameter("password")
//登录认证成功后默认转跳的路径
.defaultSuccessUrl("/home");
//关闭CSRF跨域攻击防御
http.csrf().disable();
}
2. 配置PasswordEncoder密码加密
步骤1: 首先在启动类上创建 Bean注解方法
@SpringBootApplication
public class SpringbootSecurityDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootSecurityDemoApplication.class, args);
}
//创建一个PasswordEncoder加密器存入容器中
@Bean
public PasswordEncoder getPasswordEncoder(){
return new BCryptPasswordEncoder();
}
}
步骤2: 在测试类中测试PasswordEncoder方法
@SpringBootTest
class SpringbootSecurityDemoApplicationTests {
@Autowired
private PasswordEncoder passwordEncoder;
@Test
void contextLoads() {
String pwd1 = passwordEncoder.encode("123");
String pwd2 = passwordEncoder.encode("123");
System.out.println(pwd1); //$2a$10$21Ae7HsPihdZNu.YWhqeHu4dJ5/45l5mpQxN8P3HWzPNlDeh84cm2
System.out.println(pwd2); //$2a$10$000Misx3wpASVQkzz.iyK.q9L9qx7Thl7mey/UzoLEAvVJgenvGc.
System.out.println(passwordEncoder.matches("123",pwd1));
System.out.println(passwordEncoder.matches("123",pwd2));
//$2a$10$s65QPon1AZTjZhQjlL.jF.8JBeIjdPLoL.UfZkDA5Uzv94yQhq.RG
//$2a$10$/Rs7Zr41nC6UWgP/Ij0tnOnrBwMLjwRDYrGJrNMzMMlWQB5XgmBJe
}
}
步骤3: 在配置类中使用加密的方式在内存中配置用户名和密码,修改类WebSecurityConfig
/**
* Spring Securtiy配置类
*/
@Configuration //配置类
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("tom")
.password(passwordEncoder.encode("123")).roles();
auth.inMemoryAuthentication().withUser("admin")
.password(passwordEncoder.encode("admin")).roles();
}
...
}
3. 配置动态认证
即从自定义的数据库中查询数据进行用户登录,而不是从内存中
步骤1:编写UserServiceImpl实现UserDetailsService接口
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
implements UserService, UserDetailsService {
@Autowired
private UserMapper userMapper;
/**
* 根据用户名查询用户UserDetails
* @param username
* @return
* @throws UsernameNotFoundException
*/
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//访问数据库,根据用户查询用户对象
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("username",username);
User user = userMapper.selectOne(wrapper);
if(user==null){
throw new UsernameNotFoundException("用户名不存在!");
}
//封装用户的权限集
List<GrantedAuthority> authorities = new ArrayList<>();
//封装数据库存询的用户信息
UserDetails userDetails =
new org.springframework.security.core.userdetails.User(
user.getUsername(),user.getPassword(),authorities);
return userDetails;
}
}
步骤2:添加配置类相关代码
/**
* Spring Securtiy配置类
*/
@Configuration //配置类
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private UserServiceImpl userServiceImpl;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userServiceImpl).passwordEncoder(passwordEncoder);
}
}
4. 配置异常处理
@Override
protected void configure(HttpSecurity http) throws Exception {
//开启自定义表单模式登录认证
//需要放行的url在这里配置,必须要放行/login和/login.html,不然会报错
http.authorizeRequests().antMatchers("/login", "/login.html")
.permitAll().anyRequest().authenticated()
.and().
// 设置登陆页、登录表单form中action的地址,也就是处理认证请求的路径
formLogin().loginPage("/login.html").loginProcessingUrl("/login")
//登录表单form中密码输入框input的name名,不修改的话默认是password
.usernameParameter("username").passwordParameter("password")
//登录认证成功后默认转跳的路径
.defaultSuccessUrl("/home")
//登录认证失败后请求URL ,要放行
.failureUrl("/error1").permitAll();
//关闭CSRF跨域攻击防御
http.csrf().disable();
}
注意:此处不能指定/error ,因为这个 /error是security内置的请求处理
步骤2: 编写erro1处理器
@GetMapping("/error1")
public String error(HttpServletRequest request, HttpServletResponse response){
return "error";
}
步骤3: 创建error.html模板页面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
出错了: <p th:text="${session.SPRING_SECURITY_LAST_EXCEPTION}"></p>
</body>
</html>
附加步骤4: 用户名找不到异常处理
由于spring security框架底层默认将UsernameNotFoundException设置为隐藏,而显示的是BadCredential异常,可以通过下面的方式配置实现
/**
* Spring Securtiy配置类
*/
@Configuration //配置类
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private UserServiceImpl userServiceImpl;
//重新定制DaoAuthenticationProvider
@Bean
public DaoAuthenticationProvider getDaoAuthenticationProvider(){
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
//设置用户未找到异常不隐藏
provider.setHideUserNotFoundExceptions(false);
//设置认证管理器使用UserDetaisService对象--此处使用的是我们自定义的业务实现类
provider.setUserDetailsService(userServiceImpl);
//设置认证管理器使用的密码检验器对象
provider.setPasswordEncoder(passwordEncoder);
return provider;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//指定认证管理器
auth.authenticationProvider(getDaoAuthenticationProvider());
}
...
}
5. 配置前后端分离认证成败的处理
5-1 认证成功的处理
登录成功之后我们是跳转到/home
控制器的,也就是跳转到home.html
但是在前后端分离的情况下,页面的跳转是交给前端去控制的,后端的控制器就不生效了,那我们应该如何实现让前端去跳转页面呢?
我们发现在认证成功后,执行的是AuthenticationSuccessHandler接口实现类:
默认为SimpleUrlAuthenticationSuccessHandler,如果定制自定义处理器,只需要实现该接口
/**
* 自定义认证成功的处理器Handler
*/
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
ResponseResult<Void> result = ResponseResult.ok();
response.setContentType("application/json;charset=utf-8");
PrintWriter out= response.getWriter();
out.write(new ObjectMapper().writeValueAsString(result)); //将对象转json输出
out.flush();
out.close();
}
}
然后配置成功的处理器:
@Override
protected void configure(HttpSecurity http) throws Exception {
//开启自定义表单模式登录认证
//需要放行的url在这里配置,必须要放行/login和/login.html,不然会报错
http.authorizeRequests().antMatchers("/login", "/login.html")
.permitAll().anyRequest().authenticated()
.and().
// 设置登陆页、登录表单form中action的地址,也就是处理认证请求的路径
formLogin().loginPage("/login.html").loginProcessingUrl("/login")
//登录表单form中密码输入框input的name名,不修改的话默认是password
.usernameParameter("username").passwordParameter("password")
//登录认证成功后默认转跳的路径
//.defaultSuccessUrl("/home")
// 前后端分离认证成功的处理器 -输出json
.successHandler(myAuthenticationSuccessHandler)
.failureUrl("/error1").permitAll();
//关闭CSRF跨域攻击防御
http.csrf().disable();
}
5-2 认证失败的处理
同样的,有登录成功的处理器就有登录失败的处理器,但是登录失败的情况比较多,所以需要经过很多的判断,登录失败处理器主要用来对登录失败的场景(密码错误、账号锁定等…)做统一处理并返回给前台统一的json返回体
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response, AuthenticationException e)
throws IOException, ServletException {
//定义响应的结果对象
ResponseResult<String> result = null;
if(e instanceof UsernameNotFoundException){
result = ResponseResult.error(ResultCode.USER_ACCOUNT_NOT_EXIST);
}else if (e instanceof AccountExpiredException) {
//账号过期
result = ResponseResult.error(ResultCode.USER_ACCOUNT_EXPIRED);
} else if (e instanceof BadCredentialsException) {
//凭证不对 错误
result = ResponseResult.error(ResultCode.USER_CREDENTIALS_ERROR);
} else if (e instanceof CredentialsExpiredException) {
//密码过期
result = ResponseResult.error(ResultCode.USER_CREDENTIALS_EXPIRED);
} else if (e instanceof DisabledException) {
//账号不可用
result = ResponseResult.error(ResultCode.USER_ACCOUNT_DISABLE);
} else if (e instanceof LockedException) {
//账号锁定
result = ResponseResult.error(ResultCode.USER_ACCOUNT_LOCKED);
} else{
result = ResponseResult.error(ResultCode.COMMON_FAIL);
}
response.setContentType("application/json;charset=UTF-8");
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Method", "POST,GET");
response.setContentType("application/json;charset=utf-8");
PrintWriter out= response.getWriter();
out.write(new ObjectMapper().writeValueAsString(result)); //将对象转json输出
out.flush();
out.close();
}
}
添加配置:
@Override
protected void configure(HttpSecurity http) throws Exception {
....
.successHandler(myAuthenticationSuccessHandler)
// 前后端分离认证失败的处理器 -输出json
.failureHandler(myAuthenticationFailureHandler);
//关闭CSRF跨域攻击防御
http.csrf().disable();
}
6. 配置前后端分离用户未登录的处理
而在前后端分离的情况下(比如前台使用VUE或JQ等)我们需要的是在前台接收到"用户未登录"的提示信息,所以我们接下来要做的就是屏蔽重定向的登录页面,并返回统一的json格式的返回体。而实现这一功能的核心就是实现AuthenticationEntryPoint并在WebSecurityConfig中注入,然后在configure(HttpSecurity http)方法中。AuthenticationEntryPoint主要是用来处理匿名用户访问无权限资源时的异常(即未登录,或者登录状态过期失效)
//自定义用户未登录的处理器
@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
//重新定义未登录的处理
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
ResponseResult<Void> result = ResponseResult.error(ResultCode.USER_NOT_LOGIN);
response.setContentType("application/json;charset=utf-8");
PrintWriter out= response.getWriter();
out.write(new ObjectMapper().writeValueAsString(result)); //将对象转json输出
out.flush();
out.close();
}
}
配置用户未登录的处理
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
...
// 前后端分离认证成功的处理器 -输出json
.successHandler(myAuthenticationSuccessHandler)
// 前后端分离认证失败的处理器 -输出json
.failureHandler(myAuthenticationFailureHandler)
.and()
// 前后端分离处理未登录请求
.exceptionHandling()
.authenticationEntryPoint(myAuthenticationEntryPoint);
//关闭CSRF跨域攻击防御
http.csrf().disable();
}
7. 鉴权
步骤1:在配置类上添加注解配置
@EnableWebSecurity
//@EnableGlobalMethodSecurity(jsr250Enabled = true) //开启Security注解鉴权
//@EnableGlobalMethodSecurity(securedEnabled = true)
@EnableGlobalMethodSecurity(prePostEnabled = true) //sprintSecurity自带 可以支持Spring EL表达式
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
...
}
步骤2:在控制器方法上使用注解,表示必须拥有该注解标识的权限才能访问
@RestController
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/userList")
//一旦使用此注解,表示请求该方法的用户权限集里必须该权限标识符
//@RolesAllowed("ROLE_teacher:list") //访问到数据库表中的权限标识符必须以ROLE_开头,注解上的ROLE_可以省略
//@Secured("ROLE_teacher:list") //访问到数据库表中的权限标识符必须以ROLE_开头 注解上的ROLE_不能省略
//@PreAuthorize("hasAnyAuthority('teacher:list')") //使用hashAnyAuthority EL表达式,可以指定权限标识,不要求使用ROL_ 开头
//@PreAuthorize("hasAnyRole('ROLE_teacher:list')") //使用hashAnyROLE EL表达式,可以指定权限标识,要求使用ROL_ 开头 ,数据库表中的权限标识也必须以ROLE开头
@PreAuthorize("hasRole('ROLE_teacher:list')")
public List<User> queryUserList(){
return userService.list(null);
}
}
步骤3:权限不足的处理方案
/**
* 权限不足的处理
*/
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
ResponseResult<Void> result = ResponseResult.error(ResultCode.NO_PERMISSION);
response.setContentType("application/json;charset=utf-8");
PrintWriter out= response.getWriter();
out.write(new ObjectMapper().writeValueAsString(result)); //将对象转json输出
out.flush();
out.close();
}
}
步骤4:配置类
http.authorizeRequests().antMatchers("/login", "/login.html")
.permitAll().anyRequest().authenticated()
.and().
// 设置登陆页、登录表单form中action的地址,也就是处理认证请求的路径
formLogin().loginPage("/login.html").loginProcessingUrl("/login")
//登录表单form中密码输入框input的name名,不修改的话默认是password
.usernameParameter("username").passwordParameter("password")
//登录认证成功后默认转跳的路径
//.defaultSuccessUrl("/home")
// 前后端分离认证成功的处理器 -输出json
.successHandler(myAuthenticationSuccessHandler)
// 前后端分离认证失败的处理器 -输出json
.failureHandler(myAuthenticationFailureHandler)
.and()
// 前后端分离处理未登录请求
.exceptionHandling().authenticationEntryPoint(myAuthenticationEntryPoint)
// 前后端分离处理权限不足的请求
.accessDeniedHandler(myAccessDeniedHandler);
//.failureUrl("/error1").permitAll();
//关闭CSRF跨域攻击防御
http.csrf().disable();
8. 整合JWT
步骤1:添加依赖jar
<!--用于生成JWT依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.8.1</version>
</dependency>
步骤2:编写工具类JwtTokenUtil并测试
public class JwtTokenUtil {
/**
* 过期时间50分钟
*/
private static final long EXPIRE_TIME = 5 * 60 * 10000;
/**
* jwt 密钥
*/
private static final String SECRET = "woniuxy";
/*
生成签名 50分钟过期
*/
public static String createSign(String userName) throws Exception {
try {
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(SECRET);
return JWT.create()
// 将 user id 保存到 token 里面
.withAudience(userName)
// 50分钟后token过期
.withExpiresAt(date)
//.withClaim()
//.withSubject(userName)
// token 的密钥
.sign(algorithm);
}catch(Exception ex){
ex.printStackTrace();
throw new Exception("签名错误");
}
}
/**
* 根据token获取username
* @param token
* @return
*/
public static String getUserId(String token) {
try {
String userId = JWT.decode(token).getAudience().get(0);
return userId;
} catch (JWTDecodeException e) {
throw new JWTDecodeException("生成的token 异常");
}
}
/**
* 校验token 是否有效
* @param token
* @return
*/
public static boolean checkSign(String token) {
try {
Algorithm algorithm = Algorithm.HMAC256(SECRET);
JWTVerifier verifier = JWT.require(algorithm)
// .withClaim("username", username)
.build();
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (JWTVerificationException exception) {
throw new RuntimeException("token 无效,请重新获取");
}
}
public static void main(String[] args) throws Exception {
//测试生成Token串
String strToken = JwtTokenUtil.createSign("zhangsan");
System.out.println(strToken);
//eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJ6aGFuZ3NhbiIsImV4cCI6MTY1NzA5MzQ3MX0.OdoENj363dPW2YVQfrc4SigoYlt45ydEtkgIc4xzzRo
//验证 token是否有效
boolean isValid = JwtTokenUtil.checkSign("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJ6aGFuZ3NhbiIsImV4cCI6MTY1NzA5MzQ3MX0.OdoENj363dPW2YVQfrc4SigoYlt45ydEtkgIc4xzzRo");
System.out.println(isValid);
//从给定的token串获取用户信息
String username = JwtTokenUtil.getUserId("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJ6aGFuZ3NhbiIsImV4cCI6MTY1NzA5MzQ3MX0.OdoENj363dPW2YVQfrc4SigoYlt45ydEtkgIc4xzzRo");
System.out.println(username);
}
}
步骤3:自定义认证成功的处理器Handler,用于登录认证成功后生成token并返回
**
* 自定义认证成功的处理器Handler
*/
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
try {
//获取当前登录认证成功的用户名
String username = request.getParameter("username");
String strToken = JwtTokenUtil.createSign(username);
//通过响应的json返回客户端
ResponseResult<String> result = new ResponseResult<>(strToken,"OK",200);
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
//将对象转json输出
out.write(new ObjectMapper().writeValueAsString(result));
out.flush();
out.close();
}catch(Exception e){
e.printStackTrace();
}
}
}
第2步,携带Token发送请求
要想使FilterSecurityInterceptor过滤器放行:
1. Spring Security 上下文( Context ) 中要有一个 Authentication Token ,且应该是已认证状态。
2. Authentication Token 中所包含的 User 的权限信息要满足访问当前 URI 的权限要求。
实现思路:
关键在于:在 FilterSecurityInterceptor 之前 要有一个 Filter 将用户请求中携带的 JWT 转化为 Authentication Token 存在 Spring Security 上下文( Context )中给 “后面” 的 FilterSecurityInterceptor 用。
基于上述思路,
步骤4:我们要自定义实现一个 Filter :
/**
* 将用户请求中携带的 JWT 转化为 Authentication Token
* 存入 Spring Security 上下文( Context )
* 表示每次请求只执行该过滤器一次
*/
@Component
public class JwtTokenAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private UserMapper userMapper;
@Autowired
private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
//获取当前请求的uri
String uri = request.getRequestURI();
//如果是认证请求
if(uri.endsWith("/login")){
//放行
filterChain.doFilter(request,response);
return;
}
//不是认证请求--获取请求中的头部的token串
String strToken = request.getHeader("strToken");
if(StringUtils.isEmpty(strToken)){
//抛出自定义异常 -Token为null
myAuthenticationFailureHandler.onAuthenticationFailure(request,response,
new TokenIsNullException("Token为空!"));
return ;
}
//不是空,且不是认证请求
try {
//检验token是否有效
if (JwtTokenUtil.checkSign(strToken)) {
//获取token中的用户名
String username = JwtTokenUtil.getUserId(strToken);
//查询数据库获取用户的权限集
List<String> percodes = userMapper.getPerCodesByPerm(username);
List<GrantedAuthority> authorities = new ArrayList<>();
percodes.forEach(percode->{
authorities.add(new SimpleGrantedAuthority(percode));
});
//封装数据库存询的用户信息
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken
= new UsernamePasswordAuthenticationToken(username,"",authorities);
//存入securityContext
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
//放行
filterChain.doFilter(request,response);
}
}catch(Exception e){
///抛出自定义异常 Token无效
myAuthenticationFailureHandler.onAuthenticationFailure(request,response,
new TokenIsInvalidException("Token无效!"));
}
}
}
步骤5:然后将过滤器插入到FilterChainPrxoy代理的过滤器链中的UsernamePasswordAuthencationFilter前面
//将自定义的JwtTokenAuthenticationFilter插入到过滤器链中的指定的过滤器前面
http.addFilterBefore(jwtTokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
9. 注销成功处理方案
步骤1:自定义注销成功的处理器MyLogoutSuccessHandler实现LogoutSuccessHandler接口
//注销成功的处理器
@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
String headerToken = request.getHeader("strToken");
System.out.println("logout header Token:"+headerToken);
if(!StringUtils.isEmpty(headerToken)){ //如果token不是空
SecurityContextHolder.clearContext(); //清空上下文 用户名与权限集UsernamePasswordAuthenticationToken
ResponseResult<String> result = new ResponseResult<>("","注销成功",200);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(new ObjectMapper().writeValueAsString(result));
}else{
ResponseResult<Void> result = ResponseResult.error(ResultCode.TOKEN_IS_NULL);
response.setContentType("application/json;charset=utf-8");
PrintWriter out= response.getWriter();
out.write(new ObjectMapper().writeValueAsString(result)); //将对象转json输出
out.flush();
out.close();
}
}
}
步骤2:在WebSecurityConfig配置类中 配置注销成功处理器
// 前后端分离处理注销成功操作
.and().logout().logoutSuccessHandler(myLogoutSuccessHandler);
//关闭session最严格的策略 -JWT认证的情况下,不需要security会话参与
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);