Security——Token认证
主要就讲解一下,如何使用Security实现token认证,顺带也讲讲默认的登录,是如何实现的。
主要目的是:了解Security在用户认证方面,给我们提供了哪些接口,都是做什么的。
登录流程
简单的登录流程如下:
filter拦截登录请求;provider验证密码以及其它信息;验证成功走success回调,失败走failure回调。
登录成功之后的操作:
1、如果是token认证,成功之后需要写回token,之后客户端的每一个请求,都需要携带token,此外,还需要一个独立的filter,拦截所有的请求,判断token是不是有效的。
2、如果是session,那就往session中存储用户信息。
AuthenticationToken
使用Security登录时,需要将用户信息封装成Authentication,Authentication包含了登录所需的关键参数,整个认证流程都会有Authentication的参与。
1、AuthenticationToken是Authentication的子类,刚开始学习时不要因为名称,就把它们看做是两个不同的对象;
2、这个对象包含了用户的账号、密码,以及其它登录所需的的信息;
3、这个对象是有状态变化的,“未认证的” 和 “已经完成认证的”(这里实际上说的是setAuthenticated(false)函数);
4、设计Authentication类的时候,除非设计需要,否则尽量避免采用继承的写法,避免Token被其它Provider解析(不要继承其它实现类,不是指Security提供的接口)。
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
/**
* 直接复制{@link UsernamePasswordAuthenticationToken}全部源码,根据自己的需求进行代码扩展;
* 注意{@link UsernamePasswordAuthenticationToken}的构造函数,2个参数的和3个参数的,效果是不一样的;
* 因为每一个Token都有对应的Provider,最好避免采用继承的方式写Token。
*
* @author Mr.css
* @date 2021-12-23 10:51
*/
public class AuthenticationToken extends AbstractAuthenticationToken {
//这里省略全部代码
}
AbstractAuthenticationProcessingFilter
功能类似于Servlet的Filter,代码执行的优先级非常高,即使没有配置Sevlet或者Controller,代码也可以执行。
代码执行最终,需要返回用户的认证信息(已经认证完毕),如果认证成功,继续走成功的回调接口,如果认证失败,就走失败的接口。
1、想看Security默认的功能实现,可以参考UsernamePasswordAuthenticationFilter代码;
2、主要功能,就是拦截登录请求,发起用户认证,最终返回已经完成认证的Authentication;
3、代码不会自动调用Provider,需要手动执行super.getAuthenticationManager().authenticate(authentication)函数。
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 登录验证
*
* @author Mr.css
* @date 2021-12-23 11:41
*/
public class LoginFilter extends AbstractAuthenticationProcessingFilter {
LoginFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
/**
* 尝试认证,获取request中的数据,发起认证
*
* @param request -
* @param response -
* @return returning a fully populated Authentication object (including granted authorities)
* @throws AuthenticationException -
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String userName = request.getParameter("userName");
String pwd = request.getParameter("pwd");
System.out.println("结果过滤器拦截...");
AuthenticationToken authentication = new AuthenticationToken(userName, pwd);
//发起认证,经过程序流转,最终会到达Provider
return super.getAuthenticationManager().authenticate(authentication);
}
}
AuthenticationProvider
Provider包含两个主要功能,一个是查询,一个就是认证,找到用户的详细信息,然后证明用户的账号、密码都是有效的。
这个类包含两个函数:
supports(Class<?> authentication)用于说明当前的Provider可以解析哪些Authentication,
authenticate(Authentication authentication)认证用户信息,参数与返回值类型完全一致,完成认证之后,可以将参数直接返回。
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;
/**
* 用户认证
*
* @author Mr.css
* @date 2021-12-23 10:53
*/
public class AuthenticationProvider extends DaoAuthenticationProvider {
/**
* 标明当前Provider能够处理的Token类型
*
* @param authentication tokenClass
* @return boolean
*/
@Override
public boolean supports(Class<?> authentication) {
return AuthenticationToken.class == authentication;
}
/**
* 身份鉴权
*
* @param authentication 身份证明
* @return Authentication 已经完成的身份证明(a fully authenticated object including credentials)
* @throws AuthenticationException e
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
System.out.println("身份认证:" + authentication);
return authentication;
}
}
UserDetailsService
相当于DAO,主要就是负责用户身份信息查询,包括密码、权限,下面代码是生产环境直接扣出来的,提供参考代码,按需调整。
import cn.seaboot.admin.user.bean.entity.Role;
import cn.seaboot.admin.user.bean.entity.User;
import cn.seaboot.admin.user.bean.entity.UserGroup;
import cn.seaboot.admin.user.service.PermService;
import cn.seaboot.admin.user.service.RoleService;
import cn.seaboot.admin.user.service.UserGroupService;
import cn.seaboot.admin.user.service.UserService;
import cn.seaboot.common.core.CommonUtils;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* 查询用户详细信息
*
* @author Mr.css
* @date 2020-05-08 0:02
*/
@Configuration
public class CustomUserDetailsService implements UserDetailsService {
@Resource
private PasswordHelper passWordHelper;
@Resource
private UserService userService;
@Resource
private RoleService roleService;
@Resource
private UserGroupService userGroupService;
@Resource
private PermService permService;
/**
* 因为security自身的设计原因,角色权限前面需要添加ROLE前缀
*/
private static final String ROLE_PREFIX = "ROLE_";
/**
* 默认添加一个权限,名称为登录,标明必须登录才能访问(个性化设计:只是为了方便组织代码逻辑)
*/
private static final String ROLE_LOGIN = "ROLE_LOGIN";
/**
* 因为security自身的设计原因,我们在用户分组和角色权限,增加ROLE前缀
*
* @param role 角色
* @return SimpleGrantedAuthority
*/
private SimpleGrantedAuthority genSimpleGrantedAuthority(String role) {
if (!role.startsWith(ROLE_PREFIX)) {
role = ROLE_PREFIX + role;
}
return new SimpleGrantedAuthority(role);
}
/**
* 用户登录并赋予权限
*
* @param userName 用户帐号
* @return UserDetails 用户详细信息
* @throws UsernameNotFoundException 抛出具体的异常
*/
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
System.out.println("登录的用户是:" + userName);
User user = userService.queryByUserCode(userName);
UserGroup userGroup = userGroupService.queryById(user.getOrgId(), user.getGroupId());
Role sysRole = roleService.queryById(user.getOrgId(), userGroup.getRoleId());
//用户权限列表
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
//添加用户组权限
if (CommonUtils.isNotEmpty(userGroup.getCode())) {
String role = userGroup.getCode();
grantedAuthorities.add(this.genSimpleGrantedAuthority(role));
}
//添加角色权限
if (CommonUtils.isNotEmpty(sysRole.getRoleCode())) {
String role = sysRole.getRoleCode();
grantedAuthorities.add(this.genSimpleGrantedAuthority(role));
}
//添加普通权限
Set<String> perms = permService.selectConcisePermsByRoleId(userGroup.getRoleId());
for (String perm : perms) {
if (CommonUtils.isNotEmpty(perm)) {
grantedAuthorities.add(new SimpleGrantedAuthority(perm));
}
}
// TODO: 测试时可以删除上面其它权限配置,这里仅提供参考
grantedAuthorities.add(new SimpleGrantedAuthority(ROLE_LOGIN));
// TODO:获取BCrypt加密的密码,按需调整,这里我用的是自己的加密算法,可以直接使用BCryptPasswordEncoder
String bCryptPassword = passWordHelper.getBCryptPassword(user.getPassword(), user.getPasswordSalt());
return new org.springframework.security.core.userdetails.User(user.getUserCode(), bCryptPassword, grantedAuthorities);
}
}
AuthenticationSuccessHandler
身份认证成功回调函数,如果普通登录,就进行页面转发,如果是token认证,就向客户端写回一个token。
import cn.seaboot.admin.mvc.Result;
import com.alibaba.fastjson.JSON;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 身份认证成功
*
* @author Mr.css
* @date 2021-12-23 11:59
*/
public class LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
/**
* Called when a user has been successfully authenticated.
* 认证成功之后调用
*
* @param request -
* @param response -
* @param authentication 认证信息
* @throws IOException -from write
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
response.setContentType("application/json;charset=UTF-8");
System.out.println("身份认证成功:" + authentication);
//TODO: 登录成功,将token写回客户端
response.getWriter().write(JSON.toJSONString(Result.succeed()));
}
}
AuthenticationFailureHandler
在认证过程中,出现认证问题,需要抛出异常,在这里统一处理。
import cn.seaboot.admin.mvc.Result;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
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.SimpleUrlAuthenticationFailureHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 登录失败异常处理
*
* @author Mr.css
* @date 2021-12-23 12:01
*/
public class LoginFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
Result result;
if (exception instanceof BadCredentialsException ||
exception instanceof UsernameNotFoundException) {
result = Result.failed("账户名或者密码输入错误!");
} else if (exception instanceof LockedException) {
result = Result.failed("账户被锁定,请联系管理员!");
} else if (exception instanceof CredentialsExpiredException) {
result = Result.failed("密码过期,请联系管理员!");
} else if (exception instanceof DisabledException) {
result = Result.failed("账户被禁用,请联系管理员!");
} else {
result = Result.failed("登陆失败!");
}
response.getWriter().write(result.toString());
}
}
TokenFilter
不管是session,还是使用token,登录成功之后,都需要一个独立的filter,拦截所有的请求,证明你已经登陆过了。
如果是token认证,就需要验证,你的每一个请求是否包含了token,并且需要验证token是否还有效。
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* 每一次请求都需要校验一次token
*
* @author Mr.css
* @date 2021-12-23 15:14
*/
public class TokenFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
String token = httpServletRequest.getHeader("Authentication");
System.out.println("token" + token);
//授权
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
//grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_LOGIN"));
//正常设计,在LoginFilter那一步就必须创建Authentication,这里为了演示,创建一个虚拟的Authentication。
AuthenticationToken authenticationToken = new AuthenticationToken("admin", "test", grantedAuthorities);
authenticationToken.setDetails(new WebAuthenticationDetails(httpServletRequest));
//添加到上下文中
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//未登录直接抛出异常,交给spring异常切面统一处理,也可以自定义其它处理方式
//throw new AccessDeniedException("登录未授权!");
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
配置类
需要注意黑名单和白名单的配置,避免直接拒绝接收登录接口。
import cn.seaboot.admin.security.bean.entity.SecurityChain;
import cn.seaboot.common.core.CommonUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.annotation.Resource;
import java.util.List;
/**
* Security configuration
*
* @author Mr.css
* @date 2020-05-07 23:38
*/
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private Logger logger = LoggerFactory.getLogger(SecurityConfiguration.class);
@Resource
private CustomUserDetailsService customUserDetailsService;
@Resource
private BCryptPasswordEncoder passwordHelper;
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
/**
* HttpSecurity相关配置
*
* @param http HttpSecurity
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//前面我们设置的登陆接口是/login,因此/login的配置是permitAll
registry.antMatchers("/login").access("permitAll");
// 添加拦截器
LoginFilter loginFilter = new LoginFilter();
TokenFilter tokenFilter = new TokenFilter();
loginFilter.setAuthenticationSuccessHandler(new LoginSuccessHandler());
loginFilter.setAuthenticationFailureHandler(new LoginFailureHandler());
loginFilter.setAuthenticationManager(super.authenticationManager());
http.addFilterAt(loginFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(tokenFilter, LoginFilter.class);
//禁用CSRF,默认用于防止CSRF攻击的设置,模版引擎中使用
http.csrf().disable();
// 基于token,所以不需要session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
//禁用掉XFrameOptions,这个配置能让IFrame无法嵌套我们的页面,可以防止盗链,
http.headers().frameOptions().disable();
}
/**
* 设置用户登录和密码加密功能
*
* @param auth AuthenticationManagerBuilder
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) {
AuthenticationProvider authenticationProvider = new AuthenticationProvider();
authenticationProvider.setUserDetailsService(customUserDetailsService);
authenticationProvider.setPasswordEncoder(passwordHelper);
auth.authenticationProvider(authenticationProvider);
}
}