文章目录
学习视频:SpringSecurity
一、知识补充
1.单点登录:
单点登录:微服务中有众多模块,你在某一个模块完成登录验证后就不需要在其它模块进行登录验证了
2.认证授权过程分析
(1)基于 Session,如果基于 Session,那么 Spring-security 会对 cookie 里的 sessionid 进行解析,找到服务器存储的 session 信息,然后判断当前用户是否符合请求的要求。但是session做单点登录并不是很方便。
(2)基于 token,(token就是按一定规则生成的包含用户信息的字符串),基于 Session则是解析出 token,然后将当前请求加入到 Spring-security 管理的权限信息中去。这是SpringSecurity微服务中最常用的方式。
3.使用token的流程
如果系统的模块众多,每个模块都需要进行授权与认证,所以我们选择基于token 的形式进行授权与认证,用户根据用户名密码认证成功,然后获取当前用户角色的一系列权限 值,并以用户名为 key,权限列表为 value 的形式存入 redis 缓存中,根据用户名相关信息 生成 token返回,浏览器将 token记录到 cookie 中,每次调用 api 接口都默认将 token携带 到 header 请求头中,Spring-security 解析 header 头获取 token 信息,解析 token 获取当前 用户名,根据用户名就可以从 redis 中获取权限列表,这样 Spring-security 就能够判断当前 请求是否有权限访问
二、微服务案例
项目工程
启动redis和nacos
1.启动redis
2.启动nacos
spring_security代码
1、工具类
1.DeafultPasswordEncoder密码处理工具类
@Component
public class DefaultPasswordEncoder implements PasswordEncoder {
//有参构造和无参构造
public DefaultPasswordEncoder() {this(-1);}
public DefaultPasswordEncoder(int strength) { }
//进行MD5加密
@Override
public String encode(CharSequence charSequence) {
return MD5.encrypt(charSequence.toString());
}
//进行密码比对
@Override
public boolean matches(CharSequence charSequence, String encodedPassword) {
return encodedPassword.equals(MD5.encrypt(charSequence.toString()));
}
}
2.TokenManager(token操做工具类)
①使用jwt根据用户名生成token ②根据token字符串得到用户信息 ③删除token
@Component
public class TokenManager {
//token有效时长
private long tokenEcpiration = 24*60*60*1000;
//编码秘钥
private String tokenSignKey = "123456";
//1 使用jwt根据用户名生成token
public String createToken(String username) {
String token = Jwts.builder().setSubject(username)//根据用户名生成token
.setExpiration(new Date(System.currentTimeMillis()+tokenEcpiration))//设置过期时间(过期时间=当前时间+有效时长)
.signWith(SignatureAlgorithm.HS512, tokenSignKey).compressWith(CompressionCodecs.GZIP).compact();//这一行写法固定
return token;
}
//2 根据token字符串得到用户信息
public String getUserInfoFromToken(String token) {
String userinfo = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token).getBody().getSubject();
return userinfo;
}
//3 删除token
public void removeToken(String token) { }
}
2、SpringSecurity相关的逻辑
1.spring_security的核心配置类TokenWebsecurityConfig
在这个类里面配置了哪些路径要认证,密码处理的工具是啥,退出登录时的处理器,自定义过滤器等等
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter {
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
private DefaultPasswordEncoder defaultPasswordEncoder;
private UserDetailsService userDetailsService;
@Autowired
public TokenWebSecurityConfig(UserDetailsService userDetailsService, DefaultPasswordEncoder defaultPasswordEncoder,
TokenManager tokenManager, RedisTemplate redisTemplate) {
this.userDetailsService = userDetailsService;
this.defaultPasswordEncoder = defaultPasswordEncoder;
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}
//设置退出的地址和token,redis操作地址
@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(new UnauthEntryPoint())//没有权限访问时调用括号里的你自己写的那个处理类
.and().csrf().disable()
.authorizeRequests()
.anyRequest().authenticated() //所有请求都要做认证
.and().logout().logoutUrl("/admin/acl/index/logout")//退出路径
.addLogoutHandler(new TokenLogoutHandler(tokenManager,redisTemplate)).and() //退出登录时的处理器
.addFilter(new TokenLoginFilter(authenticationManager(), tokenManager, redisTemplate))//添加自定义过滤器
.addFilter(new TokenAuthenticationFilter(authenticationManager(), tokenManager, redisTemplate)).httpBasic();//添加自定义过滤器
}
//调用userDetailsService和密码处理
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(defaultPasswordEncoder);
}
//不进行认证的路径,可以直接访问
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/api/**");
}
}
2.TokenLogoutHandler退出处理器
TokenWebSecurityConfig里面你配置了退出登录的处理器TokenLogoutHandler,那么下面看看TokenLogoutHandler做了什么工作:
从header里面获取token,然后让浏览器移除此token,然后从redis删除token
//退出处理器(我们要把redis里面的token删除掉)
public class TokenLogoutHandler implements LogoutHandler {
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
public TokenLogoutHandler(TokenManager tokenManager,RedisTemplate redisTemplate) {
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
String token = request.getHeader("token");//从header里面获取token
if(token != null) {
//token不为空,移除token,从redis删除token
tokenManager.removeToken(token);
//从token获取用户名
String username = tokenManager.getUserInfoFromToken(token);
redisTemplate.delete(username);
}
ResponseUtil.out(response, R.ok());//调用R.ok()方法,代表操作成功
}
}
3.UnauthEntryPoint未授权统一处理类
TokenWebSecurityConfig里面你配置了没有权限访问时的处理类UnauthEntryPoint,那么下面看看UnauthEntryPoint做了什么工作:
其实就是调用了我们自己封装的一个ResponseUtil输出了一个错误页面
public class UnauthEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
ResponseUtil.out(httpServletResponse, R.error());
}
}
4.spring_security的TokenLoginFilter认证过滤器
TokenLoginFilter针对认证的过滤器,TokenAuthenticationFilter是针对授权的过滤器
TokenWebSecurityConfig里面你配置了自定义认证过滤器TokenLoginFilter,那么下面看看TokenLoginFilter做了什么工作:
- 认证:从request里面拿到用户名、密码等信息交给SpringSecurity管理,然后SpringSecurity底层会调用我们的UserDetailService的loadUserByUsername(String username)方法查询数据库,然后SpringSecurity会进行认证
- 认证成功:得到认证成功之后用户信息,根据用户名生成token,把用户名称和用户权限列表放到redis,返回token
- 认证失败:返回错误页面
//认证过滤器
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
private AuthenticationManager authenticationManager;
public TokenLoginFilter(AuthenticationManager authenticationManager, TokenManager tokenManager, RedisTemplate redisTemplate) {
this.authenticationManager = authenticationManager;
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
this.setPostOnly(false);
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/acl/login","POST"));//设置登录路径和提交方式
}
//1 获取表单提交用户名和密码
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//获取表单提交数据
try {
User user = new ObjectMapper().readValue(request.getInputStream(), User.class);
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(),user.getPassword(), new ArrayList<>()));
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException();
}
}
//2 认证成功调用的方法
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
//认证成功,得到认证成功之后用户信息
SecurityUser user = (SecurityUser)authResult.getPrincipal();
//根据用户名生成token
String token = tokenManager.createToken(user.getCurrentUserInfo().getUsername());
//把用户名称和用户权限列表放到redis
redisTemplate.opsForValue().set(user.getCurrentUserInfo().getUsername(),user.getPermissionValueList());
//返回token
ResponseUtil.out(response, R.ok().data("token",token));
}
//3 认证失败调用的方法
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
ResponseUtil.out(response, R.error());
}
}
5.UserDetailService
TokenWebSecurityConfig里面你配置了UserDetailService,那么下面看看UserDetailService做了什么工作:
@Service("userDetailsService")
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private UserService userService;
@Autowired
private PermissionService permissionService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名查询数据库
User user = userService.selectByUsername(username);
if (user == null){
throw new UsernameNotFoundException("用户不存在");
}
com.atguigu.security.entity.User curUser = new com.atguigu.security.entity.User();
BeanUtils.copyProperties(user,curUser);
//根据用户查询用户权限列表信息
List<String> permission = permissionService.selectPermissionValueByUserId(user.getId());
SecurityUser securityUser = new SecurityUser();
securityUser.setCurrentUserInfo(curUser);
securityUser.setPermissionValueList(permission);
return securityUser;
}
}
6.spring_security的TokenAuthenticationFilter授权过滤器
TokenLoginFilter针对认证的过滤器,TokenAuthenticationFilter是针对授权的过滤器
TokenWebSecurityConfig里面你配置了自定义授权过滤器TokenAuthenticationFilter,那么下面看看TokenAuthenticationFilter做了什么工作:
- 从请求header中获取token,从token中获取用户名,从redis中获取对应的权限列表,然后根据用户名、token、权限列表组装出一个UsernamePasswordAuthenticationToken对象放入权限的上下文SecurityContextHolder中
public class TokenAuthenticationFilter extends BasicAuthenticationFilter {
private RedisTemplate redisTemplate;
private TokenManager tokenManager;
public TokenAuthenticationFilter(AuthenticationManager authManager, TokenManager tokenManager,RedisTemplate redisTemplate) {
super(authManager);
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
//获取认证成功当前用户的授权信息
logger.info("================="+request.getRequestURI());
if(request.getRequestURI().indexOf("admin") == -1) {
chain.doFilter(request, response);
return;
}
UsernamePasswordAuthenticationToken authentication = null;
try {
authentication = getAuthentication(request);
} catch (Exception e) {
ResponseUtil.out(response, R.error());
}
if (authentication != null) {
//放入权限的上下文中
SecurityContextHolder.getContext().setAuthentication(authentication);
} else {
ResponseUtil.out(response, R.error());
}
chain.doFilter(request, response);
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
//从header中获取token
String token = request.getHeader("token");
if (token != null || !"".equals(token.trim())) {
//从token中获取用户名
String username = tokenManager.getUserInfoFromToken(token);
if(!StringUtils.isEmpty(username)){
//从redis中获取对应的权限列表
List<String> premissionValueList = (List<String>) redisTemplate.opsForValue().get(username);
Collection<GrantedAuthority> authorities = new ArrayList<>();
for (String s : premissionValueList) {
if (StringUtils.isEmpty(s)) continue;
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(s);
authorities.add(authority);
}
return new UsernamePasswordAuthenticationToken(username, token, authorities);
}
return null;
}
return null;
}
}
3、api_gateway
跨域问题
@Configuration
public class CorsConfig {
//解决跨域
@Bean
public CorsWebFilter corsWebFilter() {
CorsConfiguration config = new CorsConfiguration();
//该服务器运行任何类型的请求,允许任何域名来源的请求,允许携带任何请求头
config.addAllowedMethod("*");
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
source.registerCorsConfiguration("/**",config);
return new CorsWebFilter(source);
}
}