基于JWT和Shiro登录认证的系统

自定义异常类

虽然JDK自带exception这个异常类,但是我们要开发的是一个JavaWeb项目,返回给客户端的异常除了包含正常的异常消息外,还需要包含状态码,还需我们自己创建异常类。

自定义异常类继承的父类,我没有选择Exception。因为Exception类型的异常,我们必须要手动显式处理,要么上抛,要么捕获。我希望我定义的异常采用既可以采用显式处理,也可以隐式处理。所以我选择继承RuntimeException这个父类。RuntimeException类型的异常可以被虚拟机隐式处理,这样就省去了我们很多手动处理异常的麻烦。

为什么要继承RuntimeException?

  • Exception类型的异常必须手动处理
  • RuntimeException异常既可以自动处理,也可以受到处理

包含的属性,状态码、异常消息

@Data //lombok的data注解 其中的getter、setter方法可以通过这个注解生成
    public class EmosException extends RuntimeException{
        private String msg;
        private int code = 500;
        public EmosException(String msg) {
            super(msg);
            this.msg = msg;
        }

        public EmosException(String msg, Throwable e) { //重载构造器,子类super调用父类的构造器完成初始化
            super(msg,e);
            this.msg = msg;
        }

        public EmosException(String msg, int code) {
            super(msg,code);
            this.msg = msg;
            this.code = code;
         }
        
        public EmosException(String msg, int code, Throwable e) {
            super(msg, e);
            this.msg = msg;
            this.code = code;
        }
    }

封装返回Web对象

JavaWeb项目需要统一数据返回格式。业务状态码、业务消息、业务数据

导入Httpcomponents库(定义了很多状态码,免去我们自定义状态码常量)

虽然SpringMVC的controller可以自动把对象转换成JSON返回给客户端,但是我们需要制定一个统一的标准,保证所有Controller返回的数据格式一致。最简便的办法就是自定义封装类,来统一封装返回给客户端的数据。

修改pom.xml文件,添加依赖库。Apache的httpcomponents库里面的HttpStatus类封装了很多状态码,所以我们在Web返回对象中封装状态码,可以用到这些状态码

<dependency>
  <groupId>org.apache.httpcomponents</groupId>
  <artifactId>httpCore</artifactId>
  <version>4.4.13></version>
</dependency>

  • 创建R类
public class R extends HashMap<String,Object> {
    public R() {
        put("code", HttpStatus.SC_OK); // 200 继承了HashMap有put方法
        put("msg","success");
    }
    public R put(String key, Object value) { //因为继承的put方法不能链式调用,不能连续调用put方法、所以再定义put方法可以链式调用
        super.put(key,value);
        return this;
    }

    public static R ok() { //每次都要new一个R对象,非常不方便,所以定义静态工厂方法
         return new R();
    }
    public static R ok(String msg) {
        R r = new R();
        r.put("msg",msg); //绑定的业务消息覆盖之前的业务消息
        return r;
    }
    public static R ok(Map<String, Object> map) {
        R r = new R();
        r.putAll(map);
        return r;
    }

    public static R error(int code, String msg) {
         R r = new R();
         r.put("code",code);
         r.put("msg",msg);
         return r;
    }

    public static R error(String msg) {
       return error(HttpStatus.SC_INTERNAL_SERVER_ERROR,msg);
    }
     public static R error() {
       return error(HttpStatus.SC_INTERNAL_SERVER_ERROR,"未知异常,请联系管理员");
    }
}

整合Swagger2

  • 添加依赖库
<dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>
  • SwaggerConfig 
@Configuration
@EnableSwagger2
    public class SwaggerConfig {
        @Bean
        public Docket createRestApi() {
            Docket docket = new Docket(DocumentaticType.SWAGGER_2);
            ApiInfoBuilder builder  = new ApiInfoBuilder();
            builder.title("EMOS在线办公系统");
            ApiInfo info = builder.build();
            docket.apiInfo(info);

            ApiSelectorBuilder selectorBuilder = docket.select();
            selectorBuilder.paths(PathSelectors.any()); //让包里所有加注解的类添加到Swagger里
            selectorBuilder.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class));
            docket = selectorBuilder.build();

            //让swagger支持jwt 单点登录每次发送请求都要带上令牌 
            ApiKey apiKey = new ApiKey("token","token","header");
            List<ApiKey> apiKeyList = new ArrayList<>();
            apiKeyList.add(apiKey);
            docket.securitySchemes(apiKeyList);

            //令牌的作用域 认证对象
            AuthorizationScope scope = new AuthorizationScope("global", "accessEverything");
            AuthorizationScope[] scopes = {scope};
            SecurityReference reference = new SecurityReference("token", scopes);
            List refList = new ArrayList();
            refList.add(reference);
            SecurityContext context = SecurityContext.builder().securityReferences(refList).build();
            List ctxList = new ArrayList();
            ctxList.add(context);
            docket.securityContexts(ctxList);
            return docket;
        }
    }

添加后端验证

对于客户端提交表单或者Ajax中的数据,后端的Controller必须先要做验证,然后才能使用这些数据。既然要验证数据,那么不妨我们来使用一下Validation库

  • 使用Validation库
  • 添加依赖库
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

  • 创建Form类

validation库在做后端验证的时候,要求必须用封装类(Form类)来保存客户端提交的数据,然后在封装类中,我们可以定义验证的规则,validation会执行这些规则,帮我们验证客户端提交的数据。

@ApiModel
@Data
public class TestSayHelloForm {
    @NotBlank
    @Pattern(regexp = "^[\\u4e00-\\u9fa5]{2,15}$")
    @ApiModelProperty("姓名")
    private String name;
},
  • TestController
@RestController
@RequestMappering("/test")
@APi("测试web接口")
public class TestController {
    @PostMapping("/sayHello")
    @ApiOperation("最简单的测试方法")
    public R sayHello(@Valid @RequestBody TestSayHelloForm form) { //要提交的数据封装成对象,所以要添加RequestBody注解
        return R.ok().put("message","Hello," + form.getName());
    }
}

抵御即跨站脚本(Xss)攻击

Xss攻击通常指的是通过利用网站系统保存数据的漏洞,通过巧妙的方法把恶意指令注入到网页,用户加载网页的时候就会自动执行恶意脚本。<script>alert('xss')</script>

通常情况下,我们登录到某个网站,如果网站使用HttpSession保存登录凭证,那么SessionId会以Cookie的形式保存在浏览器上。

避免xss攻击最有效的办法是对用户输入的数据进行转义,然后存储到数据库里。等到视图层渲染HTML页面的时候,转义后的文字是不会被当作js执行的,这就可以抵御xss攻击

  • 导入依赖库
<dependency>
  <groupId>cn.hutool</groupId>
  <artifactId>hutool-all</artifactId>
  <version>5.4.0</version>
</dependency>

  • 定义请求包装类

我们平时写web项目遇到的HttpServletRequest, 它其实是个接口。如果我们想要重新定义请求类,扩展这个接口是最不应该的。因为HttpServletRequest接口中的抽象方法太多了,我们逐一实现太耗费时间。

所以我们选择继承HttpServletRequestWrapper父类。

  • XssHttpServletRequsetWrapper
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
    public XssHttpServletRequestWrapper(HttpServletRequest request) {
        super(request);
    }

    @Override
    public String getParameter(String name) {
        String value = super.getParameter(name); //从请求里获得原始数据
        if(!StrUtil.hasEmpty(value)) { //如果不为空转义
            value = HtmlUtil.filter(value);
        }
        return value;
    }

    @Override
    public String[] getParameterValues(String name) {
        String[] values  = super.getParameterValues(name);
        if(values != null) {
            for(int i = 0; i < values.length; i++) {
                String value = values[i];
                if(!StrUtil.hasEmpty(value)) {
                    value = HtmlUtil.filter(value);
                }
                values[i] = value; 
            }
        }
        return values;
    }


    @Override
    public Map<String, String[]> getParameterMap() {
        Map<String, String[]> parameters = super.getParameterMap();
        LinkedHashMap<String,String[]> map = new LinkedHashMap();
        if(parameters != null) {
            for(String key:parameters.keySet()) {
                String[] values = parameters.get(Key);
                  for(int i = 0; i < values.length; i++) {
                      String value = values[i];
                   if(!StrUtil.hasEmpty(value)) {
                    value = HtmlUtil.filter(value);
                  }
                  values[i] = value; 
            }
                map.put(key,values);
            }
        }
        return map;
    }

    @Override
    public String getHeader(String name) {
        String value = super.getHeader(name);
        if(!StrUtil.hasEmpty(value)) {
            value = HtmlUtil.filter(value);
        }
        return value;
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        InputStream in = super.getInputStream(); //IO流
        InputStreamReader reader = new InputStreamReader(in, Charset.forName("UTF-8"));//字符流
        BufferedReader buffer = new BufferedReader(reader);
        StringBuffer body = new StringBuffer();
        String line = buffer.readLine();
        while (line != null) {
            body.append(line);
            line = buffer.readLine();
        }
        buffer.close();
        reader.close();
        in.close();
        Map<String, Object> map = JSONUtil.parseObj(body.toString());
        Map<String, Object> result = new LinkedHashMap<>();
        for (String key : map.keySet()) {
            Object val = map.get(key);
            if (val instanceof String) {
                if (!StrUtil.hasEmpty(val.toString())) {
                    result.put(key, HtmlUtil.filter(val.toString()));
                }
            }
            else {
                result.put(key, val);
            }
        }
        String json = JSONUtil.toJsonStr(result);
        ByteArrayInputStream bain = new ByteArrayInputStream(json.getBytes());
        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {

            }

            @Override
            public int read() throws IOException {
                return bain.read();
            }
        };
    }
}

Shiro

shiro是Java领域非常知名的认证(Authentication)与授权(Authorization)框架,用以替代JavaEE中的JAAS功能,相较于其他认证与授权框架,Shiro设计的非常简单,所以广受好评。

  • 什么是认证?

认证就是要核验用户的身份,比如说通过用户名和密码来检验用户的身份。登录之后shiro要记录用户成功登录的凭证。

  • 什么是授权?

授权是比认证更加精细度的划分用户的行为。比如说一个教务管理系统中,学生登录之后只能查看信息,不能修改信息。而班主任就可以修改学生的信息。这就是利用授权来限定不同身份用户的行为。

  • shiro靠什么做认证与授权的?

Shiro可以利用HttpSession或者Redis存储用户的登录凭证,以及角色或者身份信息。然后利用过滤器(Filter),对每个Http请求过滤,检查请求对应的HttpSession或者Redis中的认证与授权信息。如果用户没有登录,或者权限不够,那么shiro会向客户端返回错误信息。

也就是说,我们写用户登录模块时,用户登录成功后,要调用shiro保存登录凭证。然后查询用户的角色和权限,让Shiro存储起来,将来不管哪个方法需要登录访问,或者拥有特定的角色跟权限才能访问

JWT

JWT(Json Web Token),是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。JWT一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其他业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

Jwt用户认证

前端

后端

 

JWT可以用在单点登录的系统中

传统的javaWeb项目,利用HttpSession保存用户登录的凭证。如果后端系统采用了负载均衡设计,当用户在A节点成功登录,那么登录凭证保存在A节点的HttpSession中。如果用户下一个请求被负载均衡到了B节点,因为B节点上面没有用户的登录凭证,所以需要用户重新登录,这个体验太糟糕了。shiro可以实现认证与授权,但是不能实现单点登录,所以我们需要引入jwt

JWT可以兼容更多的客户端(浏览器、app、小程序以及物联网设备)

  • JWT的Token要经过加密才能返回给客户端,包括客户端上传的Token,后端项目需要验证核实,于是我们需要一个JWT工具类,用来加密Token和验证Token的有效性

导入依赖库

<dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-web</artifactId>
  <version>1.5.3</version>
</dependency>
<dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-spring</artifactId>
  <version>1.5.3</version>
</dependency>
<dependency>
  <groupId>com.auth0</groupId>
  <artifactId>java-jwt</artifactId>
  <version>3.10.3</version>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-configuration-processor</artifactId>
  <optional>true</optional>
</dependency>
<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-lang3</artifactId>
  <version>3.11</version>
</dependency>
<dependency>
  <groupId>org.apache.httpcomponents</groupId>
  <artifactId>httpcore</artifactId>
  <version>4.4.13</version>
</dependency>

  • 定义密钥和过期时间
emos:
jwt:
#密钥
secret: abc123456
#令牌过期时间(天)
expire: 5
# 令牌缓存时间(天数)
cache-expire: 10

  • 创建JWTUtil工具类
@Commponent
@Slf4j
public class JwtUtil {
        @Value("${emos.jwt.secret}")
        private String secret;

        @Value("${emos.jwt.expire}")
        private int expire;

        public String createToken(int userId) {
           Date date = DateUtil.offset(new Date(),DateField.DAY_OF_YEAR, 5);
           Algorithm algorithm = Algorithm.HMAC256(secret);
           JWTCreator.Builder builder = JWT.create();
           String token = builder.withClaim("userId",userId).withExpiresAt(date).sign(algorithm);
            return token;
        }

        public int getUserId(String token) {
        DecodedJWT jwt = Jwt.decode(token);
        jwt.getClaim("userId").asInt();
        return  userId;
        }

        public void verifierToken(String token) {
            Algorithm algorithm = Algorithm.HMAC256(secret);
            JWTVerifier verifier = Jwt.require(algorithm).build();
            verifier.verify(token);
        }
    }

把令牌封装成认证对象

上一小节,我们通过JwtUtil类可以生成Token,这个Token我们是要返回给客户端的,接下来我们要把Jwt和Shiro框架对接起来,这样Shiro框架就会拦截所有的Http请求,然后验证请求提交的Token是否有效。

ShiroConfig 把设置应用到Shiro框架

AuthenticatingFilter 拦截Http请求,验证Token

AuthorizingRealm 定义认证与授权的实现方法

AuthenticationToken 把Token封装成认证对象

客户端提交的Token不能直接交给Shiro框架,需要先封装成AuthenticationToken类型的对象,所以我们需要先创建AuthenticationToken的实现类。

public class OAuth2Token implements AuthenticationToken {
    private String token;

    public OAuth2Token(String token) {
        this.token = token;
    }
    @Override
    public Object getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return token;
    }
}

  • 创建AuthorizingRealm类的子类

OAuth2Realm类是AuthorizingRealm的实现类,我们要在这个是实现类中定义认证和授权的方法。因为认证与授权模块设计到用户模块和权限模块,现在我们还没有真正开发业务模块,所以我们这里先暂时定义空的认证去授权方法,把shiro和Jwt整合起来,后续再实现认证与授权。

@Component
    public class OAuth2Realm extends AuthorizingRealm {
        @Autowired
        private JwtUtil jwtUtil; //处理认证字符串需要用到这个util

        @Override
        public boolean supports(AuthenticationToken token) {
            return token instanceof OAuth2Token;
        }
        /*授权(验证权限时调用)*/
        @Override
        protected AuthorizationInfo deGetAuthorizationInfo(PrincipalCollection principalCollection) {
            SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); //授权对象
            //Todo 查询用户的权限列表
            //Todo 把权限列表添加到Info对象中
            return info;
        }

        /*认证(登录时调用)*/
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws Authentication {
            //Todo 从令牌中获取userId,然后检查该账户是否被冻结
            SimpleAuthenticationInfo info = new SimpleAuthenticationInfo();
            //Todo 往info对象中添加用户信息、Token字符串
            return info;
        }
    }

刷新令牌应该如何设计?

  • 为什么要刷新Token的过期时间?

我们在定义JwtUtil工具类的时候,生成的Token都有过期时间。那么问题来了,假设Token过期时间为15天,用户在第14天的时候,还可以免登录正常访问系统。但是到了第15天,用户的Token过期,于是用户需要重新登录系统。

HttpSession的过期时间比较优雅,默认为15分钟。如果用户连续使用系统,只要间隔时间不超过15分钟,系统就不会销毁HttpSession对象。Jwt的令牌过期时间能不能做成HttpSession那样超时时间,只要用户间隔操作时间不超过15天,系统就不需要用户重新登录系统。实现这种效果的方案有两种:双TokenToken缓存,这里重点讲一下Token缓存方案。

 

Token缓存方案是把Token缓存到Redis,然后设置Redis里面缓存的Token过期时间为正常Token的1倍,然后根据情况刷新Token的过期时间。

  • Token失效,缓存也不存在的情况

当第15天,用户的Token失效以后,我们让Shiro程序到Redis查看是否存在缓存的Token,如果这个Token不存在于Redis里面,就说明用户的操作间隔了15天,需要重新登录。

  • Token失效,但是缓存还存在的情况

如果Redis中存在的缓存的token,说明当前Token失效后,间隔时间还没有超过15天,不应该让用户重新登录。所以要生成新的Token返回给客户端,并且把这个Token缓存到Redis里面,这种操作成为刷新Token过期时间

客户端如何更新令牌

在我们的方案中,服务端刷新Token过期时间,其实就是生成一个新的Token给客户端。那么客户端怎么知道这次响应带回来的Token是更新过的呢?这个问题很容易解决

 

只要用户成功登录系统,当后端服务器更新Token的时候,就在响应中添加Token。客户端那边判断每次Ajax响应里面是否包含Token,如果包含,就把Token保存起来就可以了。

如何在响应中添加令牌

 

我们定义OAuth2Filter类拦截所有的Http请求,一方面它会把请求中的Token字符串提取出来,封装成对象交给Shiro框架;另一方面,它会检查Token的有效性。如果Token过期,那么会生成新的Token,分别存储在ThreadLocalTokenRedis中。

之所以要把新令牌保存到ThreadLocalToken里,是因为要向AOP切面类传递这个新令牌。虽然OAuth2Filter中有doFilterInternal()方法,我们可以得到响应并且写入新令牌。但是这样做非常麻烦,首先我们要通过IO流读取响应中的数据,然后还要把数据解析成JSON对象,最后再放入这个新令牌,如果我们定义了AOP切面类,拦截所有Web方法返回给R对象,然后在R对象里面添加新令牌,这多简单啊,但是OAuth2FilterAOP切面类之间没有调用关系,所以我们很难把新令牌传给AOP切面类。

创建存储令牌的媒介类

@Component
public class ThreadLocalToken {
    private ThreadLocal<String> local = new ThreadLocal<>();

    public void setToken(String token) {
        local.set(token);
    }

    public String getToken() {
        return local.get();
    }

    public void clear() {
        local.remove();
    }
}

创建OAuth2Filter类

注意事项:

因为在OAuth2Filter类中要读写ThreadLocal中的数据,所以OAuth2Filter类必须要设置成多例的,否则ThreadLocal将无法使用

@Component
@Scope("prototype") //多例对象
public class OAuth2Filter extends AuthenticatingFilter {
    @Autowired
    private ThreadLocalToken threadLocalToken;

    @Value("${emos.jwt.cache-expire}")
    private int cacheExpire;

    @Autowired
    private JwtUtil jwtUtil;//作令牌的校验

    @Autowired
    private RedisTemplate redisTemplate; //存到redis

    /*拦截请求之后,用于把令牌字符串封装成令牌对象*/
     @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest req= (HttpServletRequest) request;
        //获取请求token
        String token=getRequestToken(req);
        if(StrUtil.isBlank(token)){
            return null;
        }
        return new OAuth2Token(token);
    }

    /*
     *拦截请求,判断请求是否需要被Shiro处理
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        HttpServletRequest req= (HttpServletRequest) request;
        //AJax提交application/json数据的时候,会先发怵Option请求
        //这里要放行Options请求,不需要被Shiro处理
        if(req.getMethod().equals(RequestMethod.OPTIONS.name())){
            return true;
        }
        //除了Options请求之外,所有请求都要被Shiro处理
        return false;
    }

    /*
    *该方法用于处理所有应该被Shiro处理的请求
    */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest req= (HttpServletRequest) request;
        HttpServletResponse resp= (HttpServletResponse) response;
        resp.setContentType("text/html");
        resp.setCharacterEncoding("UTF-8");
        resp.setHeader("Access-Control-Allow-Credentials", "true");
        resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));

        threadLocalToken.clear();

        String token=getRequestToken(req);
        if(StrUtil.isBlank(token)){
            resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
            resp.getWriter().print("无效的令牌");
            return false;
        }
        try{
            jwtUtil.verifierToken(token);
        }catch (TokenExpiredException e){
            if(redisTemplate.hasKey(token)){
                redisTemplate.delete(token);
                int userId=jwtUtil.getUserId(token);
                token=jwtUtil.createToken(userId);
                redisTemplate.opsForValue().set(token,userId+"",cacheExpire, TimeUnit.DAYS);
                threadLocalToken.setToken(token);
            }
            else{
                resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
                resp.getWriter().print("令牌已过期");
                return false;
            }
        }catch (Exception e){
            resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
            resp.getWriter().print("无效的令牌");
            return false;
        }
        boolean bool=executeLogin(request,response);
        return bool;
    }

    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        HttpServletRequest req= (HttpServletRequest) request;
        HttpServletResponse resp= (HttpServletResponse) response;
        resp.setContentType("text/html");
        resp.setCharacterEncoding("UTF-8");
        resp.setHeader("Access-Control-Allow-Credentials", "true");
        resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
        resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
        try{
            resp.getWriter().print(e.getMessage());
        }catch (Exception exception){

        }

        return false;
    }

    @Override
    public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
        HttpServletRequest req= (HttpServletRequest) request;
        HttpServletResponse resp= (HttpServletResponse) response;
        resp.setContentType("text/html");
        resp.setCharacterEncoding("UTF-8");
        resp.setHeader("Access-Control-Allow-Credentials", "true");
        resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
        super.doFilterInternal(request, response, chain);

    }

    private String getRequestToken(HttpServletRequest request){
        String token=request.getHeader("token");
        if(StrUtil.isBlank(token)){
            token=request.getParameter("token");
        }
        return token;
    }
}

创建ShiroConfig类

我们要创建ShiroConfig类,是用来把OAuth2Filter和OAuth2Realm配置到Shiro框架,这样才能生效。

@Configuration
public class ShiroConfig {

    @Bean("securityManager")
    public SecurityManager securityManager(OAuth2Realm realm){
        DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
        securityManager.setRealm(realm);
        securityManager.setRememberMeManager(null);
        return securityManager;
    }

    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager,OAuth2Filter filter){
        ShiroFilterFactoryBean shiroFilter=new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);

        Map<String , Filter> map=new HashMap<>();
        map.put("oauth2",filter);
        shiroFilter.setFilters(map);

        Map<String,String> filterMap=new LinkedHashMap<>();
        filterMap.put("/webjars/**", "anon");
        filterMap.put("/druid/**", "anon");
        filterMap.put("/app/**", "anon");
        filterMap.put("/sys/login", "anon");
        filterMap.put("/swagger/**", "anon");
        filterMap.put("/v2/api-docs", "anon");
        filterMap.put("/swagger-ui.html", "anon");
        filterMap.put("/swagger-resources/**", "anon");
        filterMap.put("/captcha.jpg", "anon");
        filterMap.put("/user/register", "anon");
        filterMap.put("/user/login", "anon");
        filterMap.put("/test/**", "anon");
        filterMap.put("/meeting/recieveNotify", "anon");
        filterMap.put("/**", "oauth2");

        shiroFilter.setFilterChainDefinitionMap(filterMap);

        return shiroFilter;

    }

    @Bean("lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
        return new LifecycleBeanPostProcessor();
    }

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

利用切面类把更新的令牌返回给客户端

我们在写OAuth2Filter的时候,把更新后的令牌写到ThreadLocal里面。那么这个小节,我们要创建AOP切面类,拦截所有Web方法的返回值,在返回R对象中添加更新后的令牌。

@Aspect
@Component
public class TokenAspect {
    @Autowired
    private ThreadLocalToken threadLocalToken;

    @Pointcut("execution(public * com.example.emos.wx.controller.*.*(..))")
    public void aspect(){

    }

    @Around("aspect()")
    public Object around(ProceedingJoinPoint point) throws Throwable{
        R r=(R)point.proceed(); //方法执行结果
        String token=threadLocalToken.getToken();
        //如果ThreadLocal中存在Token,说明是更新的Token
        if(token!=null){
            r.put("token",token); //往响应中放置Token
            threadLocalToken.clear();
        }
        return r;
    }
}

精简返回给客户端的异常

@Slf4j
@RestControllerAdvice
public class ExceptionAdvice {
    @ResponseBody
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler(Exception.class)
    public String exceptionHandler(Exception e){
        log.error("执行异常",e);
        if(e instanceof MethodArgumentNotValidException){
            MethodArgumentNotValidException exception= (MethodArgumentNotValidException) e;
            //将错误信息返回给前台
            return exception.getBindingResult().getFieldError().getDefaultMessage();
        }
        else if(e instanceof EmosException){
            EmosException exception= (EmosException) e;
            return exception.getMsg();
        }
        else if(e instanceof UnauthorizedException){
            return "你不具备相关权限";
        }
        else{
            return "后端执行异常";
        }
    }
}

注册新用户的业务要点说明

一、激活码有什么用途?

首先大家要知道Emos系统并不是完全对外开放的在线办公系统,只有本公司内部人员才可以使用这套系统,所以Emos系统并不完全对外开放注册。

 

 

 

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

诉衷情の麻雀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值