springboot中使用Spring Security 之微服务的使用(六)


一、什么是微服务

  微服务由来 微服务最早由 Martin Fowler 与 James Lewis 于 2014 年共同提出,微服务架构风格是一种 使用一套小服务来开发单个应用的方式途径,每个服务运行在自己的进程中,并使用轻量 级机制通信,通常是 HTTP API,这些服务基于业务能力构建,并能够通过自动化部署机制 来独立部署,这些服务使用不同的编程语言实现,以及不同数据存储技术,并保持最低限 度的集中式管理。   微服务优势 (1)微服务每个模块就相当于一个单独的项目,代码量明显减少,遇到问题也相对来说比 较好解决。 (2)微服务每个模块都可以使用不同的存储方式(比如有的用 redis,有的用 mysql 等),数据库也是单个模块对应自己的数据库。 (3)微服务每个模块都可以使用不同的开发技术,开发模式更灵活。   微服务本质 (1)微服务,关键其实不仅仅是微服务本身,而是系统要提供一套基础的架构,这种架构 使得微服务可以独立的部署、运行、升级,不仅如此,这个系统架构还让微服务与微服务 之间在结构上“松耦合”,而在功能上则表现为一个统一的整体。这种所谓的“统一的整 体”表现出来的是统一风格的界面,统一的权限管理,统一的安全策略,统一的上线过 程,统一的日志和审计方法,统一的调度方式,统一的访问入口等等。 (2)微服务的目的是有效的拆分应用,实现敏捷开发和部署。    微服务认证与授权实现思路 1、认证授权过程分析 (1)如果是基于 Session,那么 Spring-security 会对 cookie 里的 sessionid 进行解析,找 到服务器存储的 session 信息,然后判断当前用户是否符合请求的要求。 (2)如果是 token,则是解析出 token,然后将当前请求加入到 Spring-security 管理的权限 信息中去   如果系统的模块众多,每个模块都需要进行授权与认证,所以我们选择基于 token 的形式 进行授权与认证,用户根据用户名密码认证成功,然后获取当前用户角色的一系列权限 值,并以用户名为 key,权限列表为 value 的形式存入 redis 缓存中,根据用户名相关信息 生成 token 返回,浏览器将 token 记录到 cookie 中,每次调用 api 接口都默认将 token 携带 到 header 请求头中,Spring-security 解析 header 头获取 token 信息,解析 token 获取当前 用户名,根据用户名就可以从 redis 中获取权限列表,这样 Spring-security 就能够判断当前 请求是否有权限访问  

二、权限管理数据模型

数据模型介绍

*添加角色
*为角色分配菜单

*添加用户

*为用户分配菜单

一共5张表:

security.sql(微服务登录中有)

拿这修改就行

 

 微服务权限管理案例主要功能:

使用技术说明

1、登录(认证)

2、添加角色

3、为角色分配菜单

4、添加用户

5、为用户分配角色

1、Maven

创建父工程:管理项目依赖版本

创建子模块:使用具体依赖

2.SpringBoot 本质就是Spring

3.MyBatisPlus 操作数据库框架

4.SpringCloud

(1)GateWay 网关

(2)注册中心 Nacos

其他技术:

Redis  Jwt  Swagger

搭建项目工程

1、创建父工程 acl_parent  :管理依赖版本

2、在父工程创建子模块

  (1)common

    *service_base:工具类

    *spring_security :权限配置

  (2)infrastructure

    *api_gateway: 网关

  (3)service

    *service_acl:权限管理微服务模块

 接下来:引入依赖(微服务登录中有)

 引入完依赖启动redis和Nacos

Nacos介绍

上面是Windows下面是Linux

 访问地址:http://localhost:8848/nacos/

默认用户名密码:nacos

工具类

编写common工具类

  1、编写common里面需要的工具类

  

 2.编写security工具类

 

 一.先写密码处理工具类

@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()));
}
}
二.token操作工具类
使用jwt生成token
//1.使用jwt根据用户名生成token
@Component
public class TokenManager {

//token有效时长
private long tokenEcpriation=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()+tokenEcpriation))
.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){
}
}

三.退出处理器
//退出处理器
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 httpServletResponse, Authentication authentication) {
//1.从header里面获取token

//2.token不为空,移除token,从redis删除token
String token = request.getHeader("token");
if (token!=null){
//移除
tokenManager.removeToken(token);

//从token获取用户名
String username = tokenManager.getUserInfoFromToken(token);
redisTemplate.delete(username);
}

}
}

未授权统一处理类
public class UnauthEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
ResponseUtil.out(httpServletResponse, R.error());
}
}



编写security认证过滤器

 准备两个实体类

User

@Data
@ApiModel(description = "用户实体类")
public class User implements Serializable {
private String username;
private String password;
private String nickName;
private String salt;
private String token;
}

SecurityUser类
@Data
@Slf4j
public class SecurityUser implements UserDetails {
//当前登录用户
private transient User currentUserInfo;
//当前权限
private List permissionValueList;
public SecurityUser() {
}
public SecurityUser(User user) {
if (user != null) {
this.currentUserInfo = user;
}
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection authorities = new ArrayList<>();
for(String permissionValue : permissionValueList) {
if(StringUtils.isEmpty(permissionValue)) continue;
SimpleGrantedAuthority authority = new
SimpleGrantedAuthority(permissionValue);
authorities.add(authority);
}
return authorities;
}
@Override
public String getPassword() {
return currentUserInfo.getPassword();
}
@Override
public String getUsername() {
return currentUserInfo.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}

在编写security认证过滤

1.认证过滤器:

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.认证成功的方法
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());
}
}

2.授权过滤器
 public class TokenAuthFilter extends BasicAuthenticationFilter {
    private TokenManager tokenManager;
private RedisTemplate redisTemplate;

public TokenAuthFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
this.tokenManager= tokenManager;
this.redisTemplate=redisTemplate;
}

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){
//1.从header获取token
String token = request.getHeader("token");
if (token!=null){
//从token获取用户名
String username = tokenManager.getUserInfoFromToken(token);

//从redis获取对应权限列表
ListpermissionValueList = (List) redisTemplate.opsForValue().get(username);
//创建一个Collection集合
Collection authority=new ArrayList<>();
//遍历
for (String permissionValue : permissionValueList) {
SimpleGrantedAuthority auth=new SimpleGrantedAuthority(permissionValue);
authority.add(auth);
}
return new UsernamePasswordAuthenticationToken(username,token,authority);
}
return null;
}
}

编写核心配置类
  

@Configuration
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 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/**", "/swagger-ui.html/**");
}
}

编写UserDetailsService
  如图:
                                      

代码做参考

                                                      
整合权限管理模块
  application.properties
  
#服务端口
server.port=8009
#服务器
spring.application.name=service-acl
#mysql数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3307/acldb?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=root

#redis连接
spring.redis.host=192.168.24.144
spring.redis.port=6379
spring.redis.database= 0
spring.redis.timeout=1800000
spring.redis.lettuce.pool.max-active=20
spring.redis.lettuce.pool.max-wait=-1
#最大阻塞等待时间(负数表示没有限制)
spring.redis.lettuce.pool.max-idle=5
spring.redis.lettuce.pool.min-idle=0
#最小空闲


#配置mapper xml文件的路径
mybatis-plus.mapper-locations=classpath:com/atguigu/aclservice/mapper/xml/*.xml
#nacos服务地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
#返回json的全局时间格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
#mybatis日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl


 前端页面:

                                    

 前端启动命令: npm run dev

 前端启动报如下错误:

使用npm run dev 运行,出现如下错误 :

Failed to compile.

  ./src/styles/index.scss (./node_modules/css-loader??ref--11-1!./node_modules/postcss-loader/lib??ref--11-2!./node_modules/sass-loader/lib/loader.js??ref--11-3!./src/styles/index.scss)

Module build failed (from ./node_modules/sass-loader/lib/loader.js):

Error: Missing binding E:\test\node_modules\node-sass\vendor\win32-x64-67\binding.node

Node Sass could not find a binding for your current environment: Windows 64-bit with Node.js 11.x

Found bindings for the following environments:

  - Windows 64-bit with Node.js 10.x

This usually happens because your environment has changed since running `npm install`.

Run `npm rebuild node-sass` to download the binding for your current environment.

首先在项目目录下依次安装以下文件:

npm install node-sass 

npm i node-sass -D

如果继续报错,会提醒安装element-ui就可以运行项目了,我们继续在项目目录下安装它就好了:

npm install --save element-ui

然后运行项目就OK啦,终于不报错啦!

后端错误:java.lang.NullPointerException: null

解决方法:

  

 重新启动就可以登录了