分布式项目的登录

前后端分离(分布式项目中)

理解:

1、shiro的登录流程

1 得到一个安全管理器SecurityUtils

2 在安全管理器里得到一个Subject

3 在主体里面调用subject.login(token) 该token中包含了用户名和用户密码

4 调用login 方法,shiro 底层会调用Realm 里面的认证方法

5 认证方法里面,会使用用户名称去查询用户

6 查询成功后,会把用户的名称和密码 return

7 shiro 底层会验证用户和密码

8 若shiro 验证成功,则会把用户存储在自己session 里面,其中key 是:sessionId,value是登录的用户对象

2、session的理解

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jUn0wYjX-1585212804772)(assets/1583225147085.png)]

如果是前后端不分离项目,就直接从cookie中获取jsessionid,通过jsessionid找到存储用户信息的session。

如果是前后端分离项目,由于前端服务器和后端服务器不是一个,所以jessionid不能放在cookie中。这时,就可以将cookie中获取的jessionid放在请求头中(安全)。但是后端项目模块较多,由于从请求头中通过jessionid获取的session不能在多个tomcat中共享,所以就需要使用redis进行存储,以实现session的共享。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IKUP12A2-1585212804773)(assets/1583226126958.png)]

3、使用shiro提供的接口

3.1、DefaultWebSessionManager接口

DefaultWebSessionManager接口从请求头中获取token,如果没有则生成一个token。

JSessionid 只有一个标识而已。可以找个别的代替它!

Shiro 给我们提供了DefaultWebSessionManager接口,实现他就可以很轻易的把JessionId 换成别的数据

3.2、SessionDAO接口(实现了shiro的session共享)

SessionDAO接口将获取的token进行存储。

Shiro的底层有个SessionDAO 的接口,该接口负责从某个地方读取Session,或写入session

4、百度案例

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9DlC99cn-1585212804774)(assets/1583226956706.png)]

利用请求头解决session共享问题。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JdnEB35j-1585212804776)(assets/1583226923889.png)]

token概要

概要:Token 是在服务端产生的。如果前端使用用户名/密码向服务端请求认证,服务端认证成功,那么在服务端会返回 Token 给前端。前端可以在每次请求的时候带上 Token 证明自己的合法地位。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-axju7c8l-1585212804778)(assets/下载.jpg)]

代码实现

引入依赖

		<!--shiro 和spring boot web 集成的方案-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-web-starter</artifactId>
        </dependency>
        <!-- redis 和shiro 整合 -->
        <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis</artifactId>
        </dependency>
 		<!--用处:存储验证码,用户权限等-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

配置文件

# 该app端口的配置
server:
  port: 8085
# 数据源配置
spring:
  datasource:
    url: jdbc:mysql://47.94.225.69:3306/ego-shop
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
  # redis的配置
  redis:
    port: 6379
    host: 47.94.225.69
# mybatis-plus的配置
mybatis-plus:
  mapper-locations: classpath:/mapper/*.xml
  type-aliases-package: com.zxm.entity
  configuration: # 日志的打印
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

登录对象的封装类

使用validator做了后端的表单校验

package com.zxm.param;

import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

/**
 * 构造登陆的请求参数
 */
@Data
public class LoginParam {

    @NotBlank(message = "登录用户名不能为null")
    @ApiModelProperty("用户名")
    private String principal;
    @NotBlank(message = "登录密码不能为null")
    @ApiModelProperty("密码")
    private String credentials;
    @NotBlank(message = "自动生成的uuid不能为空")
    @ApiModelProperty("前端生成的uuid")
    private String sessionUUID;
    @NotBlank(message = "用户填写的验证码不能为空")
    @ApiModelProperty("用户填写的验证码")
    private String imageCode;
}

LoginController类

登录的整体流程。使用了redis存储验证码

package com.zxm.controller;

import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.CircleCaptcha;
import cn.hutool.captcha.LineCaptcha;
import com.zxm.param.LoginParam;
import io.swagger.annotations.ApiOperation;
import lombok.SneakyThrows;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AccountException;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.CredentialsException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.pam.UnsupportedTokenException;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

/**
 * 前后端分离开发里面,登陆成功后,后端不需要进行跳转。
 */
@ApiOperation(value = "登录的数据接口")
@RestController  // 相当于@RequestBody+@RequestMapping
public class LoginController {
    @Autowired
    private StringRedisTemplate redisTemplate;

    private static final String VALIDATE_CODE_PREFIX = "validate:";

    /**
     * 补全shiro自动配置类里面的两个路径
     */
    @GetMapping("/login")
    public ResponseEntity<String> toLogin() {
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("用户还没有登录,请先登录!");
    }

    @GetMapping("/unauthorizedUrl")
    public ResponseEntity<String> unauthorizedUrl() {
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("该用户权限不够,请联系管理员提升权限!");
    }

    /**
     * 完成shiro的登录功能
     *
     * @RequestParam表示请求参数不能为空。
     */
    @ApiOperation("登录")
    @PostMapping("/login")
    public ResponseEntity<String> doLogin(
            @RequestBody @Valid LoginParam loginParam
    ) {
        try {
            checkValidateCode(loginParam);
            UsernamePasswordToken token = new UsernamePasswordToken(loginParam.getPrincipal(), loginParam.getCredentials());
            Subject subject = SecurityUtils.getSubject();

            subject.login(token);
            // 登录成了,它把用户的登录信息写在sessions 里面,而sessions 里面key时SessionId
            String sessionId = subject.getSession().getId().toString();
            return ResponseEntity.ok(sessionId);
        } catch (AccountException e) { // 用户不存在
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("用户名不存在,或用户名错误");
        } catch (CredentialsException credentialsException) { // 密码错误
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("密码过期或错误");
        } catch (UnsupportedTokenException unsupportedTokenException) { // token 不支持
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("TOKEN 不被支持");
        } catch (AuthenticationException validateCodeException) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(validateCodeException.getMessage());
        } catch (RuntimeException runtimeException) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("服务器错误");
        }
    }

    @ApiOperation(value = "获取验证码")
    @SneakyThrows  // 自动关闭资源
    @GetMapping("/captcha.jpg")
    public ResponseEntity<Void> getCaptcha(
            @RequestParam(required = true) String uuid,
            HttpServletResponse response
    ) {
        // 使用hutool工具类生成一个验证码图片
        LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(200, 50, 4, 3);
        String validateCode = lineCaptcha.getCode();// 获取验证码,将其存储在redis中。
        redisTemplate.opsForValue().set(VALIDATE_CODE_PREFIX + uuid, validateCode, 1, TimeUnit.MINUTES);
        ServletOutputStream outputStream = null;
        try {
            outputStream = response.getOutputStream();
            lineCaptcha.write(outputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return ResponseEntity.ok().build();
    }

    /**
     * 校验用户的验证码
     */
    private void checkValidateCode(LoginParam loginParam) {
        String sessionUUID = loginParam.getSessionUUID();
        String imageCode = loginParam.getImageCode();
        if (StringUtils.hasText(sessionUUID) && StringUtils.hasText(imageCode)) {
            String code = redisTemplate.opsForValue().get(VALIDATE_CODE_PREFIX + sessionUUID);
            if (imageCode.equals(code)) {
                redisTemplate.delete(VALIDATE_CODE_PREFIX + sessionUUID);
                return;
            }
        }
        throw new AuthenticationException("验证码错误,请重新获取!");
    }

    @ApiOperation(value = "登出")
    @PostMapping("/sys/logout")
    public ResponseEntity<Void> logout(){
        Subject subject = SecurityUtils.getSubject();
        subject.logout();
        return ResponseEntity.ok().build();
    }
}

UserRelam类

实现用户的认证和授权

package com.zxm.shiro;

import com.zxm.entity.SysUser;
import com.zxm.service.SysUserService;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.io.ByteArrayOutputStream;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

@Component
public class UserRealm  extends AuthorizingRealm {

    @Autowired
    private SysUserService sysUserService;
    @Autowired
    private RedisTemplate redisTemplate;
    /**
     * 登录认证只进行一次
     * @param token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        /**
         * 用户的信息保存在token中,由subject.login(token)方法传递过来的。
         */
        String username = token.getPrincipal().toString();
        // 根据用户名查询用户信息
        SysUser sysUser = sysUserService.queryUserByUserName(username);
        if(sysUser==null){
            return null; // shiro 会处理null的情况,底层会抛一个用户不存在的异常
        }
        // 屏蔽用户的私人信息后,把用户给它
        sysUser.setEmail("*******");
        ByteSource byteSource = ByteSource.Util.bytes("zxm");  // 盐可以写死,也可以从数据库中获取。sysUser.getSalt();
        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(sysUser, sysUser.getPassword(), byteSource, sysUser.getUsername());
        return simpleAuthenticationInfo;
    }

    /**没有url地址  但是需要权限访问时,都会访问该方法。
     * 做授权
     * 授权可能运行多次
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        SysUser sysUser = (SysUser) principals.getPrimaryPrincipal();
        Set<String> perms = (Set<String>) redisTemplate.opsForValue().get("AUTH_PERMS:" + sysUser.getUserId());
//            simpleAuthorizationInfo.setRoles();
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.setStringPermissions(perms);
        return simpleAuthorizationInfo;
    }
}

TokenSessionManager类

获取请求头中的token,如果请求头中没有token,则重新生成一个token

package com.zxm.shiro;

import org.apache.shiro.session.mgt.DefaultSessionManager;
import org.apache.shiro.session.mgt.SessionKey;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;
import java.util.UUID;

/**
 * 登陆成功的标识,每次访问资源都会携带这个标识。
 */
@Component
public class TokenSessionManager extends DefaultWebSessionManager {

    private static final String AUTH_HEADER = "EGO_TOKEN";

    /**
     * shiro 默认的行为,是从request 里面取出tomcat 的sessionId 来做为 key
     * @param request
     * @param response
     * @return
     */
    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        /**
         * app的token从数据库中取到存储到header中。
         * pc端token从cookie中取到存储到header中。
         * 不管是app还是pc端,都将标识存储在头中,使用头可以屏蔽差异。
         */
        String token = WebUtils.toHttp(request).getHeader(AUTH_HEADER);
        if(StringUtils.hasText(token)){  //  若请求中携带token,就直接返回token,否则生成一个token。
            return token;
        }
        token = UUID.randomUUID().toString();
        return token;
    }
}

ShiroCustomConfiguration类

shiro的配置类。

package com.zxm.config;

import com.zxm.shiro.TokenSessionManager;
import com.zxm.shiro.UserRealm;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.crazycake.shiro.IRedisManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;

import javax.sound.sampled.Port;
import java.util.HashMap;
import java.util.Map;

/**
 * 自定义shiro的配置
 */
@Configuration
public class ShiroCustomConfiguration {
    @Value("${spring.redis.host}")
    private String redisHost;

    @Value("${spring.redis.port}")
    private Integer redisPort;
    /**
     * 安全管理器
     * @return
     */
    @Bean
    public DefaultWebSecurityManager defaultWebSecurityManager(
            UserRealm userRealm,   // realm负责登陆和授权
            TokenSessionManager tokenSessionManager, // 负责token的管理
            RedisSessionDAO sessionDAO, // SessionDAO负责token的存储  使用其子类RedisSessionDAO也行。
            CredentialsMatcher credentialsMatcher // 负责密码的校验
    ){
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        userRealm.setCredentialsMatcher(credentialsMatcher);
        defaultWebSecurityManager.setRealm(userRealm);  // 自定义realm
        tokenSessionManager.setSessionDAO(sessionDAO);// session怎么存储由sessionDAO决定
        defaultWebSecurityManager.setSessionManager(tokenSessionManager);
        return defaultWebSecurityManager;
    }

    /**
     * 使用redis存储session的具体实现类
     * @param redisManager
     * @return
     */
    @Bean
    public RedisSessionDAO redisSessionDAO(IRedisManager redisManager){
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager); // 去存储在哪个redis里面
        redisSessionDAO.setExpire(7*24*3600);  // 设置session的过期时间
//        redisSessionDAO.setKeyPrefix();  // 设置存储在redis里面的前缀,默认是redis:session:
        return redisSessionDAO;
    }

    /**
     * 决定使用哪个redis
     *
     * @return
     */
    @Bean
    public IRedisManager iRedisManager(){
        RedisManager redisManager = new RedisManager();
        if(StringUtils.hasText(redisHost)){
            if(redisPort != null){
                redisManager.setHost(redisHost+":"+redisPort);
            }else{
                redisManager.setHost(redisHost+":"+6379);
            }
        }
        return redisManager;
    }
    /**
     *  密码的验证器
     * @return
     */
    @Bean
    public CredentialsMatcher credentialsMatcher(){
        // 底层使用MD5加密
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher("MD5");
        // 散列2次
        hashedCredentialsMatcher.setHashIterations(2);
        return hashedCredentialsMatcher;
    }

    /**
     * 资源放行管理
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 用户没有登录访问某个url地址时,shiro会把用户重定向到这个login页面
        shiroFilterFactoryBean.setLoginUrl("login");
        // 用户登录成功访问没有授权的页面时,会重定向到这个unauthorizedUrl页面
        shiroFilterFactoryBean.setUnauthorizedUrl("unauthorizedUrl");
        // 登录成功后,跳转到那个页面里面,在前后端分离开发里面,不使用,因为前端的跳转,由前端自己的js 去完成
//        shiroFilterFactoryBean.setSuccessUrl("");
        Map<String, String> filterChainDefinitionMap = new HashMap<>();
        filterChainDefinitionMap.put("/unauthorizedUrl","anon") ;
        filterChainDefinitionMap.put("/login","anon"); // 放行的资源
        filterChainDefinitionMap.put("/captcha.jpg","anon");  // 放行验证码
        filterChainDefinitionMap.put("/**","authc"); // 拦截的资源
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
        return shiroFilterFactoryBean;
    }
    
    /**
     * 加入注解的使用。例如使用注解监控用户重要操作。
     * 不加入这个则注解不生效!
     * Spring Boot start Aop = Spring aop + aspectjweaver
     * DefaultAdvisorAutoProxyCreator是spring aop里面的一个类
     *
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
        /**
         * true: 使用Cglib的代理方式
         * false: 使用jdk的代码方式
         * 这个东西是自己new
         * 在shiro 也有该对象的创建形式,但是shiro 里面默认为false ,为jdk ,当为jdk ,以及集成spring boot aop 后,controller 就失效了
         * 注意:shiro也有自己对注解的切面。默认使用jdk代理方式,如果想使用aop的切面注解,则必须使用cglib的动态代理。
         */
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);// 使用cgilb 给Shiro 创建一个代理对象,而不是JDK
        return advisorAutoProxyCreator;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
}

发布了29 篇原创文章 · 获赞 0 · 访问量 2244

猜你喜欢

转载自blog.csdn.net/rootDream/article/details/105122616