1. 开启Security
@EnableWebSecurity
@Configuration
public class UserWebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Resource
private PasswordEncoder passwordEncoder;
@Resource
private LoginAuthenticationSuccessHandler successHandler;
@Resource
private LoginAuthenticationFailureHandler failureHandler;
/**
* 请求网页未登录时抛出的异常信息
*/
@Resource
private MessageAuthenticationEntryPoint entryPoint;
@Resource
private UserDetailsServiceImpl detailsService;
@Resource
private UserLoginAuthorizationTokenFilter tokenFilter;
private final String[] AUTH_WHITELIST = new String[]{"/user/auth/login", "/user/login", "/oauth/login"};
//认证管理器
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(detailsService)
.passwordEncoder(passwordEncoder);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.formLogin(n -> n
.loginPage("/user/login")
.loginProcessingUrl("/user/auth/login")
.successHandler(successHandler)
.failureHandler(failureHandler))
.logout(n -> n
.logoutUrl("/user/auth/logout"))
.exceptionHandling(n->n.authenticationEntryPoint(entryPoint))
.sessionManagement(n->n.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeRequests(n -> n
.antMatchers(AUTH_WHITELIST).permitAll()
.antMatchers("/user/api/**").authenticated());
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/admin/**");
}
/**
* 注册密码加密器
*
* @return 密码加密器
*/
@Bean
public PasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
2. 配置登录验证UserDetailsService
package com.nineya.user.service;
import com.nineya.tool.text.CheckText;
import com.nineya.user.dao.UserMapper;
import com.nineya.user.entity.NineyaUserDetails;
import com.nineya.user.entity.User;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Collection;
/**
* @author 殇雪话诀别
* 2020/11/22
*/
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
private UserMapper userMapper;
@Resource
@Lazy
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = new User();
if (CheckText.checkPhone(username)) {
user.setPhone(username);
} else if (CheckText.checkMail(username)) {
user.setMail(username);
} else if (CheckText.checkNineyaId(username)) {
user.setNineyaId(username);
} else {
throw new UsernameNotFoundException(String.format("账号 %s 格式有误!", username));
}
user = userMapper.getUser(user);
if (user == null) {
throw new UsernameNotFoundException(String.format("账号 %s 不存在", username));
}
user.setPassword(passwordEncoder.encode(user.getPassword()));
return new NineyaUserDetails(user);
}
}
3. 自定义登录成功和失败处理器
登录成功处理器,这里只是简单demo,记录一下,直接响应了authentication
,这里可以通过JWT打印个token
给客户端,等等...
ResponseResult
是自定义的REsult接口统一风格,和security无关,无须理会;adminTokenUtil
为自己封装的token管理器,生成了一个jwt凭证,同时将token的id在Redis存底,如果id不存在该token就不在生效,token的有效期不在jwt中存放,由服务端控制,这样是为了防止密码丢失,token无法提前失效的情况;ObjectMapper
是springboot的json转换工具,可以直接使用@Resource
注入。
package com.nineya.user.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nineya.tool.restful.ResponseResult;
import com.nineya.user.entity.AdminDetails;
import com.nineya.user.util.AdminTokenUtil;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* @author 殇雪话诀别
* 2020/11/22
*/
@Component
public class LoginAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Resource
private AdminTokenUtil adminTokenUtil;
@Resource
private ObjectMapper mapper;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
Map<String, Object> data = new HashMap<>();
ResponseResult result = ResponseResult.access(data);
response.setContentType("application/json;charset=UTF-8");
AdminDetails details = (AdminDetails) authentication.getPrincipal();
data.put("admin_token", adminTokenUtil.createToken(details.getAid()));
response.getWriter().write(mapper.writeValueAsString(result));
}
}
登录失败处理器:
package com.nineya.user.handler;
import com.alibaba.fastjson.JSONObject;
import com.nineya.tool.restful.ResponseResult;
import com.nineya.tool.restful.ResultCode;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
/**
* @author 殇雪话诀别
* 2020/11/22
*/
@Component
public class LoginAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
ResponseResult result = new ResponseResult().setError(true);
if (exception instanceof UsernameNotFoundException) {
// 用户不存在
result.setCode(ResultCode.SUCCESS.getCode());
result.setMessage(exception.getMessage());
} else if (exception instanceof BadCredentialsException) {
// 密码错误
result.setCode(ResultCode.SUCCESS.getCode());
result.setMessage(exception.getMessage());
} else if (exception instanceof LockedException) {
// 用户被锁
result.setCode(ResultCode.SUCCESS.getCode());
result.setMessage(exception.getMessage());
} else {
// 系统错误
result.setCode(ResultCode.SERVER_ERROR.getCode());
result.setMessage(exception.getMessage());
}
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSONObject.toJSONString(result));
}
}
4. 创建访问失败处理器
访问接口时,因为登录失效,没有权限被访问等情况的处理器
package com.nineya.user.handler;
import com.alibaba.fastjson.JSONObject;
import com.nineya.tool.restful.ResponseResult;
import com.nineya.tool.restful.ResultCode;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author 殇雪话诀别
* 2020/11/25
*
* 响应未登录消息
*/
@Component
public class MessageAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
ResponseResult result = new ResponseResult(ResultCode.FORBIDDEN).setError(true);
if (authException instanceof UsernameNotFoundException) {
// 用户不存在
result.setMessage(authException.getMessage());
} else if (authException instanceof BadCredentialsException) {
// 密码错误
result.setMessage(authException.getMessage());
} else if (authException instanceof LockedException) {
// 用户被锁
result.setMessage(authException.getMessage());
} else if (authException instanceof InsufficientAuthenticationException) {
// 用户token失效
result.setMessage("用户未登录");
} else {
// 系统错误
result.setCode(ResultCode.SERVER_ERROR.getCode());
result.setMessage(authException.getMessage());
}
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSONObject.toJSONString(result));
}
}
5. 创建token解析过滤器
SecurityContextHolder.getContext().setAuthentication(authentication);
表示添加授权,仅对当前请求有效,每个被security拦截的请求都会执行该过滤器;
简单示例,使用时可通过token查询数据库、Redis,或者解析JWT等方式判断登录状态,然后决定是否添加authentication
。
package com.nineya.user.filter;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.nineya.tool.text.CheckText;
import com.nineya.user.entity.AdminDetails;
import com.nineya.user.util.AdminTokenUtil;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author 殇雪话诀别
* 2020/11/26
*/
@Component
public class AdminLoginAuthorizationTokenFilter extends OncePerRequestFilter {
@Resource
private AdminTokenUtil adminTokenUtil;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
final String token = httpServletRequest.getHeader("Admin-Authorization");
if (CheckText.isEmpty(token)) {
filterChain.doFilter(httpServletRequest, httpServletResponse);
return;
}
DecodedJWT decodedJWT = adminTokenUtil.verifyToken(token);
long aid = decodedJWT.getClaim("aid").asLong();
long createTime = decodedJWT.getIssuedAt().getTime();
if (adminTokenUtil.valid(aid, createTime)) {
AdminDetails adminDetails = new AdminDetails(aid);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(adminDetails, null, adminDetails.getAuthorities());
// 在上下文中记录UserDetails
SecurityContextHolder.getContext().setAuthentication(authentication);
httpServletRequest.setAttribute("aid", aid);
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}