文章目录
一、springsecurity的工作流程
上手之前先看一下springsecurity的工作流程:
Java设计模式中有一种责任链模式,而spring security就是基于这种模式,利用多个过滤器Filter,组成一条过滤链,针对web请求进行安全验证,同时会将验证的信息封装成身份令牌:Authentication,在访问受保护的资源时,通过来校验上下文中的Authentication来判断是否允许访问。
基于前后端分离的情况下,对于spring security在项目中的使用,一般都是后端提供登录接口给到前端,当用户登录成功返回token或者cookie,前端拿着令牌访问后端资源。
再来认识一些重要的类:
Authentication:认证通过构建的身份令牌
public interface Authentication extends Principal, Serializable {
//角色权限标识集合
Collection<? extends GrantedAuthority> getAuthorities();
//一般存放我们的密码
Object getCredentials();
//一般存放用户信息
Object getDetails();
//获取用户身份信息,在未认证的情况下获取到的是用户名,在已认证的情况下获取到的是 UserDetails
Object getPrincipal();
//是否认证 true false
boolean isAuthenticated();
//设置isAuthenticated
void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
Authentication是一个接口,下面还有不同的具体实现;
SecurityContext:上下文对象,我们的上面讲到的令牌Authentication就是放在这个里面:
public interface SecurityContext extends Serializable {
//只有俩个方法,get/set
Authentication getAuthentication();
void setAuthentication(Authentication var1);
}
SecurityContextHolder:获取SecurityContext的静态工具类
AuthenticationManager:内部的authenticate方法针对web请求进行认证,放回认证的Authentication对象。
简单描述下springsecurity的认证流程就是:
1、将请求丢给AuthenticationManager的authenticate方法进行认证
2、认证成功后返回Authentication对象
3、SecurityContextHolder获取SecurityContext对象
4、将Authentication放入SecurityContext中
二、springboot+springsecurity+jwt
方案:对于前段后分离的情况下,再加上微服务,cookie-session 的方案可能不太友好,除非服务端将session做成服务节点共享,放到缓存等等,除此以外jwt令牌、或者token+redis也能在微服务中作为登录方案,后端提供登录接口给到前端,认证成功则返回jwt令牌,后续请求所有接口前端都要携带上jwt令牌,后端接受到请求需要在springsecurity中添加filter对令牌做解析,放行请求。
1、项目准备
pom.xml
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.3.1.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</dependency>
</dependencies>
2、编写过滤器AuthenticationLoginFilter
public class AuthenticationLoginFilter extends AbstractAuthenticationProcessingFilter {
/**
* 构造方法,调用父类的,设置登录地址/login,请求方式POST
*/
public AuthenticationLoginFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
//获取表单提交数据
String username = request.getParameter("username");
String password = request.getParameter("password");
//封装到token中提交
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
return getAuthenticationManager().authenticate(authRequest);
}
}
这个过滤器相当于我们定义了登录接口,AbstractAuthenticationProcessingFilter本身有了一个子类UsernamePasswordAuthenticationFilter继承,我们这里选择重写attemptAuthentication方法,可以让我们直接访问到/login接口,我们再来看一下springsecurity内部UsernamePasswordAuthenticationFilter本身的实现:
public class UsernamePasswordAuthenticationFilter extends
AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
private boolean postOnly = true;
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
和我们AuthenticationLoginFilter没什么区别,在springsecurity配置中,需要将我们写的AuthenticationLoginFilter 添加到UsernamePasswordAuthenticationFilter的前面,并且在AuthenticationLoginFilter 注入其需要的bean。
3、编写登录成功/失败处理器
在认证成功或者认证失败,都会调用相对应的处理器执行成功/失败方法
1、登录成功处理器LoginAuthenticationSuccessHandler
@Component
@Slf4j
public class LoginAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private JwtUtils jwtUtils;
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Authentication authentication) throws IOException {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
SecurityContextHolder.getContext().setAuthentication(authentication);
//生成令牌
String accessToken = jwtUtils.createToken(userDetails.getUsername());
renderToken(httpServletResponse, LoginToken.builder().accessToken(accessToken).build());
}
/**
* 渲染返回 token 数据,因为前端页面接收的都是Result对象,故使用application/json返回
*/
public void renderToken(HttpServletResponse response, LoginToken token) throws IOException {
ResponseUtils.result(response, R.ok(token, "登录成功"));
}
}
工具类 ResponseUtils 和 响应体:
public class ResponseUtils {
public static void result(HttpServletResponse response, R msg) throws IOException {
response.setContentType("application/json;charset=UTF-8");
ServletOutputStream out = response.getOutputStream();
ObjectMapper objectMapper = new ObjectMapper();
out.write(objectMapper.writeValueAsString(msg).getBytes("UTF-8"));
out.flush();
out.close();
}
}
返回响应体
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class R<T> implements Serializable {
private static final long serialVersionUID = 1L;
@Getter
@Setter
/**
* 返回标记:成功标记=0,失败标记=1
*/
private int code;
@Getter
@Setter
/**
* 返回信息
*/
private String msg;
@Getter
@Setter
/**
* 数据
*/
private T data;
public static <T> R<T> ok() {
return restResult(null, CommonConstants.SUCCESS, null);
}
public static <T> R<T> ok(T data) {
return restResult(data, CommonConstants.SUCCESS, null);
}
public static <T> R<T> ok(T data, String msg) {
return restResult(data, CommonConstants.SUCCESS, msg);
}
public static <T> R<T> failed() {
return restResult(null, CommonConstants.FAIL, null);
}
public static <T> R<T> failed(String msg) {
return restResult(null, CommonConstants.FAIL, msg);
}
public static <T> R<T> failed(T data) {
return restResult(data, CommonConstants.FAIL, null);
}
public static <T> R<T> failed(T data, String msg) {
return restResult(data, CommonConstants.FAIL, msg);
}
private static <T> R<T> restResult(T data, int code, String msg) {
R<T> apiResult = new R<>();
apiResult.setCode(code);
apiResult.setData(data);
apiResult.setMsg(msg);
return apiResult;
}
}
public interface CommonConstants {
/**
* 成功标记
*/
Integer SUCCESS = 0;
/**
* 失败标记
*/
Integer FAIL = 1;
}
2、登录失败处理器LoginAuthenticationFailureHandler
@Component
public class LoginAuthenticationFailureHandler implements AuthenticationFailureHandler {
/**
* 一旦登录失败则会被调用
*
* @param httpServletRequest
* @param response
* @param exception 这个参数是异常信息,可以根据不同的异常类返回不同的提示信息
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest,
HttpServletResponse response,
AuthenticationException exception) throws IOException {
//TODO 根据项目需要返回指定异常提示,陈某这里演示了一个用户名密码错误的异常
//BadCredentialsException 这个异常一般是用户名或者密码错误
if (exception instanceof BadCredentialsException) {
ResponseUtils.result(response, R.failed("用户名或密码不正确"));
}
ResponseUtils.result(response, R.failed("登录失败"));
}
}
4、实现UserDetailsService
UserDetailsService主要就是为了那用户名称去我们的数据库查询,这里我们需要自己去实现再注入到springsecurity
UserDetailsServiceImpl:
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if ("root".equals(username)){
SecurityUser userDetails = new SecurityUser();
userDetails.setUsername("root");
userDetails.setPassword(passwordEncoder.encode("root"));
//角色
SimpleGrantedAuthority grantedAuthorityRole = new SimpleGrantedAuthority("ROLE_ADMIN");
List<GrantedAuthority> list = new ArrayList<>();
list.add(grantedAuthorityRole);
userDetails.setAuthorities(list);
return userDetails;
}
throw new UsernameNotFoundException("用户不存在");
}
}
因为这里需要连接数据库,为了节省时间,直接写死 root 账号。
5、权限不足和未登录访问资源处理器
1、权限不足处理器RequestAccessDeniedHandler
@Component
public class RequestAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException {
ResponseUtils.result(response, R.failed("权限不足"));
}
}
2、未登录访问资源处理器EntryPointUnauthorizedHandler
@Component
public class EntryPointUnauthorizedHandler implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
ResponseUtils.result(response,R.failed("无权限,请先登录"));
}
}
6、Jwt验证过滤器TokenAuthenticationFilter
public class TokenAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtUtils jwtUtils;
/**
* UserDetailsService的实现类,从数据库中加载用户详细信息
*/
@Qualifier("userDetailsServiceImpl")
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
String token = request.getHeader("token");
/**
* token存在则校验token
* 1. token是否存在
* 2. token存在:
* 2.1 校验token中的用户名是否失效
*/
if (!StringUtils.isEmpty(token)){
String username = jwtUtils.getUsernameFromToken(token);
//SecurityContextHolder.getContext().getAuthentication()==null 未认证则为true
if (!StringUtils.isEmpty(username) && SecurityContextHolder.getContext().getAuthentication()==null){
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
//如果token有效
if ("root".equals(userDetails.getUsername())){
// 将用户信息存入 authentication,方便后续校验
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null,
userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 将 authentication 存入 ThreadLocal,方便后续获取用户信息
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
//继续执行下一个过滤器
chain.doFilter(request,response);
}
}
7、springsecurity配置
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private EntryPointUnauthorizedHandler entryPointUnauthorizedHandler;
@Autowired
private RequestAccessDeniedHandler requestAccessDeniedHandler;
/**
* 登录成功处理器
*/
@Autowired
private LoginAuthenticationSuccessHandler loginAuthenticationSuccessHandler;
/**
* 登录失败处理器
*/
@Autowired
private LoginAuthenticationFailureHandler loginAuthenticationFailureHandler;
/**
* 加密
*/
@Autowired
private PasswordEncoder passwordEncoder;
/**
* userDetailService
*/
@Qualifier("userDetailsServiceImpl")
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtAuthenticationSecurityConfig jwtAuthenticationSecurityConfig;
@Autowired
private AuthenticationManager authenticationManager;
@Override
protected void configure(HttpSecurity http) throws Exception {
AuthenticationLoginFilter filter = new AuthenticationLoginFilter();
filter.setAuthenticationManager(authenticationManager);
//认证成功处理器
filter.setAuthenticationSuccessHandler(loginAuthenticationSuccessHandler);
//认证失败处理器
filter.setAuthenticationFailureHandler(loginAuthenticationFailureHandler);
//将这个过滤器添加到UsernamePasswordAuthenticationFilter之前执行
http.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);
//直接使用DaoAuthenticationProvider
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
//设置userDetailService
provider.setUserDetailsService(userDetailsService);
//设置加密算法
provider.setPasswordEncoder(passwordEncoder);
http.authenticationProvider(provider);
http.formLogin()
//禁用表单登录,前后端分离用不上
.disable()
//应用登录过滤器的配置,配置分离
// 设置URL的授权
.authorizeRequests()
// 这里需要将登录页面放行,permitAll()表示不再拦截,/login 登录的url
.antMatchers("/login")
.permitAll()
// anyRequest() 所有请求 authenticated() 必须被认证
.anyRequest()
.authenticated()
//处理异常情况:认证失败和权限不足
.and()
.exceptionHandling()
//认证未通过,不允许访问异常处理器
.authenticationEntryPoint(entryPointUnauthorizedHandler)
//认证通过,但是没权限处理器
.accessDeniedHandler(requestAccessDeniedHandler)
.and()
//禁用session,JWT校验不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
//将TOKEN校验过滤器配置到过滤器链中,否则不生效,放到UsernamePasswordAuthenticationFilter之前
.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class)
// 关闭csrf
.csrf().disable();
}
// 自定义的Jwt Token校验过滤器
@Bean
public TokenAuthenticationFilter authenticationTokenFilterBean() {
return new TokenAuthenticationFilter();
}
/**
* 加密算法
*
* @return
*/
@Bean
public PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
}
8、接口测试
登录接口
1、登录成功 ip:端口/login
username:root
password:root
2、登录失败
username:root
password:321456(错误密码)
携带令牌访问受保护的资源
定义一个UserController
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping()
//只有角色为'ROLE_ADMIN'
@PreAuthorize("hasRole('ROLE_ADMIN')")
public String getUser() throws JsonProcessingException {
return "root";
}
@GetMapping("/info")
//只有角色'ROLE_USER'
@PreAuthorize("hasRole('ROLE_USER')")
public String getUserInfo() throws JsonProcessingException {
return "root";
}
}
将登陆返回的access_token 携带在头部进行请求
访问/user/info
因为我们代码中赋值的角色信息为’ROLE_ADMIN’,所以访问/use成功,访问/user/info 则显示权限不足
当我们不携带token访问资源时:
提示我们需要先登录。