1.什么是微服务?

微服务架构下的动态权限配置 微服务权限管理_redis

微服务的优势:

  • 微服务的每个模块就相当于一个单独的项目,代码量明显减少,遇到问题也相对来说比较好解决。
  • 微服务每个模块都可以使用不同的存储方式(比如Redis,mysql)数据库也是单个模块对应自己的数据库
  • 微服务每个模块都可以使用不同的开发技术,开发模式增加灵活

微服务的本质

微服务架构下的动态权限配置 微服务权限管理_spring_02

2.微服务认证与授权过程:

微服务架构下的动态权限配置 微服务权限管理_微服务架构下的动态权限配置_03

微服务架构下的动态权限配置 微服务权限管理_java_04

3.微服务权限管理数据模型

菜单资源,用户,角色还有两张用户关联多对多表的关联表

不同的用户具有多个角色,每个角色也可以分配给多个用户,并且每种角色可以访问多个不同的菜单资源。

微服务架构下的动态权限配置 微服务权限管理_redis_05

4.主要用到的技术

微服务架构下的动态权限配置 微服务权限管理_redis_06

环境的需要:

安装Redis和Nacos

 

  5.项目的工程结构:

微服务架构下的动态权限配置 微服务权限管理_redis_07

 

(1)工具类:

service_base中的核心工具类

微服务架构下的动态权限配置 微服务权限管理_spring_08

spring_security中的工具类

微服务架构下的动态权限配置 微服务权限管理_java_09

DefaultPasswordEncoder类:

package com.lsy.security.security;

import com.lsy.utils.MD5;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

@Component
public class DefaultPasswordEncoder implements PasswordEncoder {

    
    //有参数和无参数的构造
    public DefaultPasswordEncoder() {
        this(-1);
    }
    public DefaultPasswordEncoder(int strength) {
    }
    //进行MD5加密
    @Override
    public String encode(CharSequence charSequence) {
        //charSequence需要加密的字符串
        return MD5.encrypt(charSequence.toString());
    }
    //进行密码比对,看输入的密码加密后与存储的密码是否一样
    @Override
    public boolean matches(CharSequence charSequence, String encodedPassword) {
        return encodedPassword.equals(MD5.encrypt(charSequence.toString()));
    }
}
Token操作工具类:TokenManager

在认证授权的过程中,我们要根据用户名生成一个Token,将这个Token放到Cookie中

TokenManager中,主要是根据用户名得到相应的Token的值,或者根据Token的值获得用门户。

使用jwt生成Token

package com.lsy.security.security;

import io.jsonwebtoken.CompressionCodecs;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;

import java.util.Date;

@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)
                .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) {
        //可以不写删除Token的方法,因为可以在客户端上不携带Token。
    }
}

 

退出处理类:TokenLogoutHandler

因为Token是每次发送请求时通过header传过来的,所以在退出的时候就是需要根据用户名得到Token,再根据Token在Redis中将Token进行删除

package com.lsy.security.security;

import com.lsy.utils.R;
import com.lsy.utils.ResponseUtil;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutHandler;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

//退出处理器
//LogoutHandler是security中提供的接口
public class TokenLogoutHandler implements LogoutHandler {
    
    
    private TokenManager tokenManager;
    //需要对Redis进行操作。
    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) {
        
        //1 从header里面获取token
        //2 token不为空,移除token,从redis删除token
        //在Redis中key存的是用户名,value存放的是权限列表。
        //所以要的到key,将其删除
        String token = request.getHeader("token");
        if(token != null) {
            //移除
            tokenManager.removeToken(token);
            //从token获取用户名
            String username = tokenManager.getUserInfoFromToken(token);
            //在Redis中删除
            redisTemplate.delete(username);
        }
        //返回数据
        ResponseUtil.out(response, R.ok());
    }
}

未经授权的统一处理类:UnauthEntryPoint 
 

package com.lsy.security.security;


import com.lsy.utils.R;
import com.lsy.utils.ResponseUtil;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class UnauthEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        ResponseUtil.out(httpServletResponse, R.error());
    }
}

编写自定义的认证与授权的过滤器:

认证过滤器:TokenLoginFilter

package com.lsy.security.filter;


import com.fasterxml.jackson.databind.ObjectMapper;
import com.lsy.security.entity.SecurityUser;
import com.lsy.security.entity.User;
import com.lsy.security.security.TokenManager;
import com.lsy.utils.R;
import com.lsy.utils.ResponseUtil;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

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;

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 {
        //认证成功,得到认证成功之后用户信息,会存放在Authentication authResult中,通过getPrincipal可以获得对应对象。
        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());
    }
}

授权过滤器:TokenAuthFilter

package com.lsy.security.filter;

import com.lsy.security.security.TokenManager;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

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.Collection;
import java.util.List;

public class TokenAuthFilter extends BasicAuthenticationFilter {

    private TokenManager tokenManager;
    private RedisTemplate redisTemplate;
    public TokenAuthFilter(AuthenticationManager authenticationManager,TokenManager tokenManager,RedisTemplate redisTemplate) {
        super(authenticationManager);
        this.tokenManager = tokenManager;
        this.redisTemplate = redisTemplate;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        //获取当前认证成功用户权限信息
        UsernamePasswordAuthenticationToken authRequest = getAuthentication(request);
        //判断如果有权限信息,放到权限上下文中
        if(authRequest != null) {
            SecurityContextHolder.getContext().setAuthentication(authRequest);
        }
        chain.doFilter(request,response);
    }

    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
        //从header获取token
        String token = request.getHeader("token");
        if(token != null) {
            //从token获取用户名
            String username = tokenManager.getUserInfoFromToken(token);
            //从redis获取对应权限列表
            List<String> permissionValueList = (List<String>)redisTemplate.opsForValue().get(username);
            Collection<GrantedAuthority> authority = new ArrayList<>();
            for(String permissionValue : permissionValueList) {
                SimpleGrantedAuthority auth = new SimpleGrantedAuthority(permissionValue);
                authority.add(auth);
            }
            return new UsernamePasswordAuthenticationToken(username,token,authority);
        }
        return null;
    }

}

security的核心配置类 TokenWebSecurityConfig

package com.lsy.security.config;

 
import com.lsy.security.filter.TokenAuthFilter;
import com.lsy.security.filter.TokenLoginFilter;
import com.lsy.security.security.DefaultPasswordEncoder;
import com.lsy.security.security.TokenLogoutHandler;
import com.lsy.security.security.TokenManager;
import com.lsy.security.security.UnauthEntryPoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;

@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;
    }

    /**
     * 配置设置
     * @param http
     * @throws Exception
     */
    //设置退出的地址和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 TokenAuthFilter(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/**");
    }
}

 

 

网关的作用:

对外暴露出统一的端口,然后用户直接访问网关,通过网关转发到具体的模块中。

使用的是SpringCloud中的一个组件gateway

api-gateway模块中的配置类:

package com.lsy.gateway.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.util.pattern.PathPatternParser;


//解决的是跨域问题。
@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);
    }
}

 接下来就是编写相应的UserDetailsService

package com.lsy.service.impl;


import com.lsy.security.entity.SecurityUser;
import com.lsy.entity.User;
import com.lsy.service.PermissionService;
import com.lsy.service.UserService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.List;

@Service("userDetailsService")
public class UserDetailsServiceImpl 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.lsy.security.entity.User curUser = new com.lsy.security.entity.User();
        BeanUtils.copyProperties(user,curUser);

        //根据用户查询用户权限列表
        List<String> permissionValueList = permissionService.selectPermissionValueByUserId(user.getId());
        SecurityUser securityUser = new SecurityUser();
        securityUser.setCurrentUserInfo(curUser);
        securityUser.setPermissionValueList(permissionValueList);
        return securityUser;
    }
}

其他相关的代码见代码连接:

 


可以通过查看Redis得到存储在Redis中的角色权限

微服务架构下的动态权限配置 微服务权限管理_微服务架构下的动态权限配置_10

在nocos中可以看到相应的服务名:

默认用户名和密码是:nacos,nacos

微服务架构下的动态权限配置 微服务权限管理_spring_11

启动前端项目就可以进行访问,没有授权的路径,是无法访问的。

微服务架构下的动态权限配置 微服务权限管理_微服务架构下的动态权限配置_12

登录授权的过程:

微服务架构下的动态权限配置 微服务权限管理_java_13

微服务架构下的动态权限配置 微服务权限管理_java_14