自定义认证(前后分离)
工具类
//主要处理redis的事务
@Component
public class RedisUtils {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private ObjectMapper objectMapper;
public <T>String setRedis(String key , T base) {
String json = null;
try {
json = objectMapper.writeValueAsString(base);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
redisTemplate.opsForValue().set(GlobalConstants.REDIS_USER_KEY_PREFIX+key,json);
return json;
}
public LoginUser getRedis(String key) throws IOException {
String json = redisTemplate.opsForValue().get(GlobalConstants.REDIS_USER_KEY_PREFIX + key);
LoginUser loginUser = null;
if(json != null){
loginUser = new LoginUser();
CustomAuthority customAuthority = objectMapper.readValue(json,CustomAuthority.class);
List<CustomAuthority> customAuthorities = objectMapper.readValue(customAuthority.getAuthority(), new TypeReference<List<CustomAuthority>>(){});
loginUser.setAuthorities(customAuthorities);
}
return loginUser;
}
public boolean delete(String key){
Boolean delete = redisTemplate.delete(GlobalConstants.REDIS_USER_KEY_PREFIX + key);
return delete;
}
}
@Component
public class WebUtils {
@Autowired
private ObjectMapper objectMapper;
public void writer(HttpServletResponse response, HttpResult result) throws IOException {
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
PrintWriter writer = response.getWriter();
writer.write(objectMapper.writeValueAsString(result));
}
}
public class JwtUtils {
public static <T>String getSecretKey(String base){
Map<String, Object> map = new HashMap<String, Object>(){
{
put("base",base);
put("expire_time", System.currentTimeMillis() + 1000 * 60 * 60 * 24);
}
};
String token = JWTUtil.createToken(map, GlobalConstants.JWT_SIGNATURE_KEY.getBytes());
return token;
}
public static Object parseToken(String token){
JWT jwt = JWTUtil.parseToken(token);
Object uid = jwt.getPayload().getClaim("base");
return uid;
}
}
//使用个这个工具类之前,我们还要引入一个依赖
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.16</version>
</dependency>
配置redis
redis:
host: localhost
port: 6379
password: 123456
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
max-wait: 200ms
database: 0
配置类
@Bean
AuthenticationManager authenticationManager(HttpSecurity httpSecurity) throws Exception {
return (AuthenticationManager) httpSecurity.getSharedObject(AuthenticationManagerBuilder.class)
.build();
}
@Bean
public SecurityFilterChain chain(HttpSecurity http) throws Exception {
http
.cors().and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/toLogin").permitAll()
.anyRequest().authenticated()
.csrf().disable();
return http.build();
}
重写方法
前端的信息会以json的方式传给后端,后端的数据也是以json的方式传递给前端。所以我们用Spring Security默认的UsernamePasswordAuthenticationFilter是不行的,所以我们第一步就是自己写一个Filter,我们来一步步分析。
@Component
public class MyUsernamePasswordLoginFilter extends AbstractAuthenticationProcessingFilter {
//首先我们就是继承AbstractAuthenticationProcessingFilter
@Autowired
private ObjectMapper objectMapper;
@Autowired
private MyLoginSuccessHandler myLoginSuccessHandler;
@Autowired
private MyLoginFailureHandler myLoginFailureHandler;
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/toLogin", "POST");//它只会拦截"/toLogin"登录的路径,和POST请求方式
private boolean postOnly = true;
public MyUsernamePasswordLoginFilter(AuthenticationManager antPathRequestMatcher) {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER,antPathRequestMatcher);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, RuntimeException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());//判断postOnly是否为真,和前端的请求是否为“POST”
} else {
User user = objectMapper.readValue(request.getInputStream(), User.class);
//request.getInputStream()获取从前端传过来的json数据
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(user.getName(), user.getPassword());//将数据进行封装
return this.getAuthenticationManager().authenticate(authRequest);
}
}
//成功后调用
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
myLoginSuccessHandler.onAuthenticationSuccess(request,response,authResult);//自定义登录成功类
}
//失败后调用
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
myLoginFailureHandler.onAuthenticationFailure(request,response,failed);//自定义登录失败类
}
}
自定义的成功和失败类
如前面少些不同,因为前后的分离了,所以我们后端就不再做跳转的功能。我们只向前端发送一个json数据,json中包含token(令牌)。以后前端发送请求必须携带token,否则就视为没有认证。
@Component
public class MyLoginSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private RedisUtils redisUtils;//redis工具类
@Autowired
private WebUtils webUtils;
@Autowired
private ObjectMapper objectMapper;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
String token = JwtUtils.getSecretKey(loginUser.getUser().getId());
redisUtils.setRedis(loginUser.getUser().getId(),objectMapper.writeValueAsString(loginUser.getAuthorities()));
webUtils.writer(response,HttpResult.ok("登录成功", HttpToken.token(token)));
}
}
@Component
public class MyLoginFailureHandler implements AuthenticationFailureHandler {
@Autowired
private WebUtils webUtils;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
HttpResult httpResult = null;
if(exception instanceof LockedException){
httpResult = HttpResult.fail("账户被锁定!",9999);
}
else if(exception instanceof BadCredentialsException){
httpResult = HttpResult.fail(exception.getLocalizedMessage(),9997);//用户名或密码错误
}
webUtils.writer(response,httpResult);
}
}
自定义注销
配置类
@Autowired
private MyLogoutSuccessHandler myLogoutSuccessHandler;
@Bean
public SecurityFilterChain chain(HttpSecurity http) throws Exception {
http
.cors().and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/toLogin").permitAll()
.antMatchers("/toLogout").permitAll()
.anyRequest().authenticated()
.and()
.logout()
.logoutUrl("/toLogout")
.logoutSuccessHandler(myLogoutSuccessHandler)
.csrf().disable();
return http.build();
}
MyLogoutSuccessHandler
@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
@Autowired
private RedisUtils redisUtils;
@Autowired
private WebUtils webUtils;
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
String token = request.getHeader(GlobalConstants.HEADER_TOKEN_KEY);//从获取前端传过来的token
if(token!=null){
Object key = JwtUtils.parseToken(token);
redisUtils.delete((String) key);
}//我获取token,从token中获取信息,根据信息删除redis中的数据
webUtils.writer(response, HttpResult.ok("注销成功"));
}
}
异常处理
配置类
@Autowired
private MyDeniedHandler myDeniedHandler;
@Autowired
private MyEntryPoint myEntryPoint;
@Bean
public SecurityFilterChain chain(HttpSecurity http) throws Exception {
http
.cors().and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/captcha").permitAll()
.antMatchers("/toLogin").permitAll()
.antMatchers("/toLogout").permitAll()
.antMatchers("/hello").anonymous()
.anyRequest().authenticated()
.and()
.logout()
.logoutUrl("/toLogout")
.logoutSuccessHandler(myLogoutSuccessHandler)
.and()
.exceptionHandling()
.accessDeniedHandler(myDeniedHandler)//处理权限异常
.authenticationEntryPoint(myEntryPoint)//处理认证异常
.and()
.csrf().disable();
return http.build();
}
}
MyDeniedHandler
@Component
public class MyDeniedHandler implements AccessDeniedHandler{
@Autowired
private WebUtils webUtils;
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
HttpResult httpResult = null;
httpResult = HttpResult.fail("权限不足",8998);
webUtils.writer(response,httpResult);
}
}
MyEntryPoint
@Component
public class MyEntryPoint implements AuthenticationEntryPoint {
@Autowired
private WebUtils webUtils;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
HttpResult httpResult = null;
httpResult = HttpResult.fail(authException.getLocalizedMessage(),7999);
webUtils.writer(response,httpResult);
}
}
自定义授权
配置类
@Autowired
private MyBasicAuthenticationFilter myBasicAuthenticationFilter;
@Bean
public SecurityFilterChain chain(HttpSecurity http) throws Exception {
http
.cors().and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/toLogin").permitAll()
.antMatchers("/toLogout").permitAll()
.anyRequest().authenticated()
.and()
.logout()
.logoutUrl("/toLogout")
.logoutSuccessHandler(myLogoutSuccessHandler)
.and()
.addFilterBefore(myBasicAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling()
.accessDeniedHandler(myDeniedHandler)
.authenticationEntryPoint(myEntryPoint)
.and()
.csrf().disable();
return http.build();
}
MyBasicAuthenticationFilter
@Component
public class MyBasicAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private RedisUtils redisUtils;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取请求头中的token
String token = request.getHeader(GlobalConstants.HEADER_TOKEN_KEY);
if(!StringUtils.hasText(token)){
filterChain.doFilter(request,response);
return;
}
Object uid = JwtUtils.parseToken(token);
LoginUser loginUser = redisUtils.getRedis(uid.toString());
if(Objects.nonNull(loginUser)){
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(uid, (Object) token, loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
filterChain.doFilter(request,response);
}
}