userName 3 minggu lalu
induk
melakukan
b522928815

+ 57 - 12
pom.xml

@@ -16,31 +16,76 @@
     <dependencies>
         <dependency>
             <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.mysql</groupId>
+            <artifactId>mysql-connector-j</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-boot-starter</artifactId>
+            <version>3.5.3.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-test</artifactId>
             <scope>test</scope>
         </dependency>
         <dependency>
-            <groupId>org.mockito</groupId>
-            <artifactId>mockito-core</artifactId>
-            <scope>test</scope>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt</artifactId>
+            <version>0.9.0</version>
         </dependency>
         <dependency>
-            <groupId>org.springframework.boot</groupId>
-            <artifactId>spring-boot-starter-web</artifactId>
+            <groupId>com.auth0</groupId>
+            <artifactId>java-jwt</artifactId>
+            <version>3.10.1</version>
         </dependency>
         <dependency>
             <groupId>org.apache.shiro</groupId>
-            <artifactId>shiro-spring-boot-web-starter</artifactId>
-            <version>1.9.1</version>
+            <artifactId>shiro-spring</artifactId>
+            <version>1.8.0</version>
         </dependency>
         <dependency>
-            <groupId>com.mysql</groupId>
-            <artifactId>mysql-connector-j</artifactId>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+            <version>3.9</version>
         </dependency>
         <dependency>
-            <groupId>com.baomidou</groupId>
-            <artifactId>mybatis-plus-boot-starter</artifactId>
-            <version>3.5.3.1</version>
+            <groupId>commons-codec</groupId>
+            <artifactId>commons-codec</artifactId>
+            <version>1.14</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-collections4</artifactId>
+            <version>4.4</version>
+        </dependency>
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>fastjson</artifactId>
+            <version>1.2.83</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-json</artifactId>
+        </dependency>
+        <!-- ini格式处理 -->
+        <dependency>
+            <groupId>org.ini4j</groupId>
+            <artifactId>ini4j</artifactId>
+            <version>0.5.4</version>
+        </dependency>
+        <!-- AOP依赖,必须,否则shiro权限拦截验证不生效 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-aop</artifactId>
         </dependency>
 
     </dependencies>

+ 159 - 0
src/main/java/com/zhentao/constants/CommonConstant.java

@@ -0,0 +1,159 @@
+
+
+package com.zhentao.constants;
+
+/**
+ * 公共常量
+ */
+public interface CommonConstant {
+
+    /**
+     * 默认页码为1
+     */
+    Long DEFAULT_PAGE_INDEX = 1L;
+
+    /**
+     * 默认页大小为10
+     */
+    Long DEFAULT_PAGE_SIZE = 10L;
+
+    /**
+     * 分页总行数名称
+     */
+    String PAGE_TOTAL_NAME = "total";
+
+    /**
+     * 分页数据列表名称
+     */
+    String PAGE_RECORDS_NAME = "records";
+
+    /**
+     * 分页当前页码名称
+     */
+    String PAGE_INDEX_NAME = "pageIndex";
+
+    /**
+     * 分页当前页大小名称
+     */
+    String PAGE_SIZE_NAME = "pageSize";
+
+    /**
+     * 分页最后一页
+     */
+    String PAGE_LAST_NAME = "lastPage";
+
+    /**
+     * 登录用户
+     */
+    String LOGIN_SYS_USER = "loginSysUser";
+
+    /**
+     * 登录token
+     */
+    String JWT_DEFAULT_TOKEN_NAME = "token";
+
+    /**
+     * JWT用户名
+     */
+    String JWT_USERNAME = "username";
+
+    /**
+     * JWT刷新新token响应状态码
+     */
+    int JWT_REFRESH_TOKEN_CODE = 460;
+
+    /**
+     * JWT刷新新token响应状态码,
+     * Redis中不存在,但jwt未过期,不生成新的token,返回361状态码
+     */
+    int JWT_INVALID_TOKEN_CODE = 461;
+
+    /**
+     * JWT Token默认密钥
+     */
+    String JWT_DEFAULT_SECRET = "888888";
+
+    /**
+     * JWT 默认过期时间,3600L,单位秒
+     */
+    Long JWT_DEFAULT_EXPIRE_SECOND = 36000L;
+
+    /**
+     * 默认头像
+     */
+    String DEFAULT_HEAD_URL = "";
+
+    /**
+     * 管理员角色名称
+     */
+    String ADMIN_ROLE_NAME = "管理员";
+
+    String ADMIN_LOGIN = "adminLogin";
+
+    /**
+     * 验证码token
+     */
+    String VERIFY_TOKEN = "verifyToken";
+
+    /**
+     * 图片
+     */
+    String IMAGE = "image";
+
+    /**
+     * JPEG
+     */
+    String JPEG = "JPEG";
+
+    /**
+     * base64前缀
+     */
+    String BASE64_PREFIX = "data:image/png;base64,";
+
+    /**
+     * ..
+     */
+    String SPOT_SPOT = "..";
+
+    /**
+     * ../
+     */
+    String SPOT_SPOT_BACKSLASH = "../";
+
+    /**
+     * SpringBootAdmin登录信息
+     */
+    String ADMIN_LOGIN_SESSION = "adminLoginSession";
+
+    /**
+     * 用户浏览器代理
+     */
+    String USER_AGENT = "User-Agent";
+
+    /**
+     * 本机地址IP
+     */
+    String LOCALHOST_IP = "127.0.0.1";
+    /**
+     * 本机地址名称
+     */
+    String LOCALHOST_IP_NAME = "本机地址";
+    /**
+     * 局域网IP
+     */
+    String LAN_IP = "192.168";
+    /**
+     * 局域网名称
+     */
+    String LAN_IP_NAME = "局域网";
+    /**
+     * 忽略appId
+     */
+    String NOT_WITH_App_Id = "notWithAppId";
+
+    String mobile="1688888888";
+
+    String password="boss1688";
+
+    Integer chains_app_id=10001;
+}

+ 39 - 12
src/main/java/com/zhentao/controller/LoginController.java

@@ -1,31 +1,58 @@
 package com.zhentao.controller;
 
 import org.apache.shiro.SecurityUtils;
-import org.apache.shiro.authc.AuthenticationException;
 import org.apache.shiro.authc.IncorrectCredentialsException;
 import org.apache.shiro.authc.UnknownAccountException;
 import org.apache.shiro.authc.UsernamePasswordToken;
 import org.apache.shiro.subject.Subject;
 import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestParam;
 
 @Controller
 public class LoginController {
 
+    // ---------------------- 登录页面 ----------------------
+    @GetMapping("/login")
+    public String loginPage() {
+        return "login"; // 对应 resources/templates/login.html
+    }
+
+    // ---------------------- 登录处理 ----------------------
     @PostMapping("/login")
-    public String login(@RequestParam String username, @RequestParam String password) {
-        Subject currentUser = SecurityUtils.getSubject();
+    public String login(String username, String password, Model model) {
+        Subject subject = SecurityUtils.getSubject();
         UsernamePasswordToken token = new UsernamePasswordToken(username, password);
         try {
-            currentUser.login(token);
-            return "redirect:/home";
-        } catch (UnknownAccountException e) { // 用户名不存在
-            return "redirect:/login?error=invalid";
-        } catch (IncorrectCredentialsException e) { // 密码错误
-            return "redirect:/login?error=invalid";
-        } catch (AuthenticationException e) { // 其他认证异常(如账户锁定)
-            return "redirect:/login?error=unknown";
+            subject.login(token); // 执行认证
+            return "redirect:/home"; // 认证成功跳转主页
+        } catch (UnknownAccountException e) {
+            model.addAttribute("error", "用户名不存在");
+        } catch (IncorrectCredentialsException e) {
+            model.addAttribute("error", "密码错误");
         }
+        return "login"; // 认证失败返回登录页
+    }
+
+    // ---------------------- 主页(需认证) ----------------------
+    @GetMapping("/home")
+    public String homePage(Model model) {
+        Subject subject = SecurityUtils.getSubject();
+        model.addAttribute("username", subject.getPrincipal());
+        return "home"; // 对应 resources/templates/home.html
+    }
+
+    // ---------------------- 未授权页面 ----------------------
+    @GetMapping("/unauthorized")
+    public String unauthorizedPage() {
+        return "unauthorized"; // 对应 resources/templates/unauthorized.html
+    }
+
+    // ---------------------- 退出登录 ----------------------
+    @GetMapping("/logout")
+    public String logout() {
+        SecurityUtils.getSubject().logout();
+        return "redirect:/login";
     }
 }

+ 73 - 0
src/main/java/com/zhentao/properties/JwtProperties.java

@@ -0,0 +1,73 @@
+
+
+package com.zhentao.properties;
+
+import com.zhentao.constants.CommonConstant;
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * JWT属性配置
+ **/
+@Data
+@Component
+@ConfigurationProperties(prefix = "spring-boot-zhentao.jwt")
+public class JwtProperties {
+
+    /**
+     * token名称,默认名称为:token,可自定义
+     */
+    private String tokenName = CommonConstant.JWT_DEFAULT_TOKEN_NAME;
+
+    /**
+     * 密码
+     */
+    private String secret = CommonConstant.JWT_DEFAULT_SECRET;
+
+    /**
+     * 签发人
+     */
+    private String issuer;
+
+    /**
+     * 主题
+     */
+    private String subject;
+
+    /**
+     * 签发的目标
+     */
+    private String audience;
+
+    /**
+     * token失效时间,默认1小时,60*60=3600
+     */
+    private Long expireSecond = CommonConstant.JWT_DEFAULT_EXPIRE_SECOND;
+
+    /**
+     * 是否刷新token,默认为true
+     */
+    private boolean refreshToken = true;
+
+    /**
+     * 刷新token倒计时,默认10分钟,10*60=600
+     */
+    private Integer refreshTokenCountdown;
+
+    /**
+     * redis校验jwt token是否存在
+     */
+    private boolean redisCheck;
+
+    /**
+     * 单用户登录,一个用户只能又一个有效的token
+     */
+    private boolean singleLogin;
+
+    /**
+     * 是否进行盐值校验
+     */
+    private boolean saltCheck;
+
+}

+ 27 - 0
src/main/java/com/zhentao/properties/ShiroPermissionProperties.java

@@ -0,0 +1,27 @@
+
+
+package com.zhentao.properties;
+
+import lombok.Data;
+
+/**
+ * Shiro权限配置映射类
+ **/
+@Data
+public class ShiroPermissionProperties {
+
+    /**
+     * 路径
+     */
+    private String url;
+    /**
+     * 路径数组
+     */
+    private String[] urls;
+
+    /**
+     * 权限
+     */
+    private String permission;
+
+}

+ 39 - 0
src/main/java/com/zhentao/properties/ShiroProperties.java

@@ -0,0 +1,39 @@
+
+
+package com.zhentao.properties;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+import java.util.List;
+
+/**
+ * Shiro配置映射类
+ **/
+@Data
+@ConfigurationProperties(prefix = "spring-boot-zhentao.shiro")
+public class ShiroProperties {
+
+    /**
+     * 是否启用
+     */
+    private boolean enable;
+
+    /**
+     * 路径权限配置
+     */
+    private String filterChainDefinitions;
+
+    /**
+     * 设置无需权限路径集合
+     */
+    private List<String[]> anon;
+
+    /**
+     * 权限配置集合
+     */
+    @NestedConfigurationProperty
+    private List<ShiroPermissionProperties> permission;
+
+}

+ 29 - 0
src/main/java/com/zhentao/shiro/config/JwtCredentialsMatcher.java

@@ -0,0 +1,29 @@
+
+
+package com.zhentao.shiro.config;
+
+import com.zhentao.shiro.utils.JwtUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.shiro.authc.AuthenticationInfo;
+import org.apache.shiro.authc.AuthenticationToken;
+import org.apache.shiro.authc.credential.CredentialsMatcher;
+
+/**
+ * JWT证书匹配
+ **/
+@Slf4j
+public class JwtCredentialsMatcher implements CredentialsMatcher {
+
+    @Override
+    public boolean doCredentialsMatch(AuthenticationToken authenticationToken, AuthenticationInfo authenticationInfo) {
+        String token = authenticationToken.getCredentials().toString();
+        String salt = authenticationInfo.getCredentials().toString();
+        try {
+            return JwtUtil.verifyToken(token, salt);
+        } catch (Exception e) {
+            log.error("JWT Token CredentialsMatch Exception:" + e.getMessage(), e);
+        }
+        return false;
+    }
+
+}

+ 172 - 0
src/main/java/com/zhentao/shiro/config/JwtFilter.java

@@ -0,0 +1,172 @@
+
+
+package com.zhentao.shiro.config;
+
+
+import com.zhentao.properties.JwtProperties;
+import com.zhentao.shiro.utils.JwtToken;
+import com.zhentao.shiro.utils.JwtTokenUtil;
+import com.zhentao.shiro.utils.JwtUtil;
+import com.zhentao.shiro.utils.SaltUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shiro.authc.AuthenticationException;
+import org.apache.shiro.authc.AuthenticationToken;
+import org.apache.shiro.subject.Subject;
+import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
+import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
+import org.apache.shiro.web.util.WebUtils;
+
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Shiro JWT授权过滤器
+ **/
+@Slf4j
+public class JwtFilter extends AuthenticatingFilter {
+    private JwtProperties jwtProperties;
+
+    public JwtFilter(   JwtProperties jwtProperties) {
+         this.jwtProperties = jwtProperties;
+    }
+
+    /**
+     * 将JWT Token包装成AuthenticationToken
+     *
+     * @param servletRequest
+     * @param servletResponse
+     * @return
+     * @throws Exception
+     */
+    @Override
+    protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
+        String url = ((ShiroHttpServletRequest) servletRequest).getRequestURI();
+        String model = JwtTokenUtil.getModel(url);
+        String token = JwtTokenUtil.getToken(model);
+        if (StringUtils.isBlank(token)) {
+            throw new AuthenticationException("token不能为空");
+        }
+        if (JwtUtil.isExpired(token)) {
+            throw new AuthenticationException("JWT Token已过期,token:" + token);
+        }
+
+        // 如果开启redis二次校验,或者设置为单个用户token登录,则先在redis中判断token是否存在
+        if (jwtProperties.isRedisCheck() || jwtProperties.isSingleLogin()) {
+            boolean redisExpired = false;
+//            if(url.startsWith("/api/admin/")){
+//                redisExpired = adminLoginRedisService.exists(token);
+//            }else if(url.startsWith("/api/shop/")){
+//                redisExpired = shopLoginRedisService.exists(token);
+//            }else if(url.startsWith("/api/supplier/")){
+//                redisExpired = supplierLoginRedisService.exists(token);
+//            }else if(url.startsWith("/api/service/")){
+//                redisExpired = serviceLoginRedisService.exists(token);
+//            }
+//            if (!redisExpired) {
+//                throw new AuthenticationException("Redis Token不存在,token:" + token);
+//            }
+        }
+
+        String username = JwtUtil.getUsername(token);
+        String salt = SaltUtil.getSalt("futuvip", jwtProperties);
+        if (jwtProperties.isSaltCheck()) {
+//            if(url.startsWith("/api/admin/")) {
+//                salt = adminLoginRedisService.getSalt(username);
+//            }else if(url.startsWith("/api/shop/")){
+//                salt = shopLoginRedisService.getSalt(username);
+//            }else if(url.startsWith("/api/supplier/")){
+//                salt = supplierLoginRedisService.getSalt(username);
+//            }else if(url.startsWith("/api/service/")){
+//                salt = serviceLoginRedisService.getSalt(username);
+//            }
+        } else {
+            salt = jwtProperties.getSecret();
+        }
+        return JwtToken.build(token, username, salt, jwtProperties.getExpireSecond());
+    }
+
+    /**
+     * 访问失败处理
+     *
+     * @param request
+     * @param response
+     * @return
+     * @throws Exception
+     */
+    @Override
+    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
+        HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
+        HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
+        // 返回-1未登录
+        String url = httpServletRequest.getRequestURI();
+        log.error("onAccessDenied url:{}", url);
+//        ApiResult<Boolean> apiResult = ApiResult.fail(ApiCode.NOT_LOGIN);
+//        HttpServletResponseUtil.printJson(httpServletResponse, apiResult);
+        return false;
+    }
+
+    /**
+     * 判断是否允许访问
+     *
+     * @param request
+     * @param response
+     * @param mappedValue
+     * @return
+     */
+    @Override
+    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
+        String url = WebUtils.toHttp(request).getRequestURI();
+        log.debug("isAccessAllowed url:{}", url);
+        if (this.isLoginRequest(request, response)) {
+            return true;
+        }
+        boolean allowed = false;
+        try {
+            allowed = executeLogin(request, response);
+        } catch (IllegalStateException e) { //not found any token
+            log.error("Token不能为空", e);
+        } catch (Exception e) {
+            log.error("访问错误", e);
+        }
+        return allowed || super.isPermissive(mappedValue);
+    }
+
+    /**
+     * 登录成功处理
+     *
+     * @param token
+     * @param subject
+     * @param request
+     * @param response
+     * @return
+     * @throws Exception
+     */
+    @Override
+    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
+        String url = WebUtils.toHttp(request).getRequestURI();
+        log.debug("鉴权成功,token:{},url:{}", token, url);
+        // 刷新token
+        JwtToken jwtToken = (JwtToken) token;
+        HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
+//        shiroLoginService.refreshToken(jwtToken, url, httpServletResponse);
+        return true;
+    }
+
+    /**
+     * 登录失败处理
+     *
+     * @param token
+     * @param e
+     * @param request
+     * @param response
+     * @return
+     */
+    @Override
+    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
+        log.error("登录失败,token:" + token + ",error:" + e.getMessage(), e);
+        return false;
+    }
+}

+ 100 - 0
src/main/java/com/zhentao/shiro/config/JwtRealm.java

@@ -0,0 +1,100 @@
+
+
+package com.zhentao.shiro.config;
+
+
+import com.zhentao.shiro.utils.JwtToken;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+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.springframework.beans.factory.annotation.Autowired;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Shiro 授权认证
+ **/
+@Slf4j
+public class JwtRealm extends AuthorizingRealm {
+
+
+
+    @Autowired
+    private HttpServletRequest request;
+
+    @Override
+    public boolean supports(AuthenticationToken token) {
+        return token != null && token instanceof JwtToken;
+    }
+
+    /**
+     * 授权认证,设置角色/权限信息
+     *
+     * @param principalCollection
+     * @return
+     */
+    @Override
+    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
+        log.debug("doGetAuthorizationInfo principalCollection..."+request.getRequestURI());
+        // 设置角色/权限信息
+        JwtToken jwtToken = (JwtToken) principalCollection.getPrimaryPrincipal();
+        // 获取username
+        String username = jwtToken.getUsername();
+        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
+        //这里应是查数据库
+        Map<String,Set<String>> map=new HashMap<>();
+        Set<String> permissions=new HashSet<>();
+        permissions.add("emp:get");
+        permissions.add("emp:add");
+        map.put("admin",permissions);
+        Set<String> pw=new HashSet<>();
+        pw.add("emp:add");
+        map.put("user001",pw);
+
+       Set<String>user_permission= map.get(username);
+       if(!user_permission.isEmpty()) {
+           authorizationInfo.setStringPermissions(user_permission);
+       }
+
+        return authorizationInfo;
+    }
+
+    /**
+     * 登录认证
+     *
+     * @param authenticationToken
+     * @return
+     * @throws AuthenticationException
+     */
+    @Override
+    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
+        log.debug("doGetAuthenticationInfo authenticationToken...");
+        // 校验token
+        JwtToken jwtToken = (JwtToken) authenticationToken;
+        if (jwtToken == null) {
+            throw new AuthenticationException("jwtToken不能为空");
+        }
+        String salt = jwtToken.getSalt();
+        if (StringUtils.isBlank(salt)) {
+            throw new AuthenticationException("salt不能为空");
+        }
+        return new SimpleAuthenticationInfo(
+                jwtToken,
+                salt,
+                getName()
+        );
+
+    }
+
+}

+ 0 - 36
src/main/java/com/zhentao/shiro/config/MyRealm.java

@@ -1,36 +0,0 @@
-package com.zhentao.shiro.config;
-
-import org.apache.shiro.authc.*;
-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.springframework.stereotype.Component;
-
-@Component
-public class MyRealm extends AuthorizingRealm {
-
-    // 授权方法
-    @Override
-    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
-        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
-        // 这里可以根据用户信息添加角色和权限
-        authorizationInfo.addRole("admin");
-        authorizationInfo.addStringPermission("user:manage");
-        return authorizationInfo;
-    }
-
-    // 认证方法
-    @Override
-    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
-        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
-        String username = upToken.getUsername();
-        String password = "123456"; // 模拟从数据库获取的密码
-
-        if (!"admin".equals(username)) {
-            throw new UnknownAccountException("用户名不存在");
-        }
-
-        return new SimpleAuthenticationInfo(username, password, getName());
-    }
-}

+ 260 - 41
src/main/java/com/zhentao/shiro/config/ShiroConfig.java

@@ -1,76 +1,295 @@
+
+
 package com.zhentao.shiro.config;
 
-import com.zhentao.shiro.config.MyRealm;
+import com.alibaba.fastjson.JSON;
+
+import com.zhentao.properties.JwtProperties;
+import com.zhentao.properties.ShiroPermissionProperties;
+import com.zhentao.properties.ShiroProperties;
+import com.zhentao.util.IniUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.collections.MapUtils;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shiro.SecurityUtils;
 import org.apache.shiro.authc.Authenticator;
+import org.apache.shiro.authc.credential.CredentialsMatcher;
+import org.apache.shiro.authc.pam.FirstSuccessfulStrategy;
 import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
-import org.apache.shiro.authz.Authorizer;
-import org.apache.shiro.authz.ModularRealmAuthorizer;
+import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
+import org.apache.shiro.mgt.DefaultSubjectDAO;
 import org.apache.shiro.mgt.SecurityManager;
-import org.apache.shiro.session.mgt.SessionManager;
-import org.apache.shiro.session.mgt.eis.MemorySessionDAO;
-import org.apache.shiro.session.mgt.eis.SessionDAO;
+import org.apache.shiro.mgt.SessionStorageEvaluator;
+import org.apache.shiro.spring.LifecycleBeanPostProcessor;
+import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
 import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
-import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
-import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition;
 import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
-import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
+import org.apache.shiro.web.mgt.DefaultWebSessionStorageEvaluator;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.web.filter.DelegatingFilterProxy;
 
-import java.util.Collections;
+import javax.servlet.DispatcherType;
+import javax.servlet.Filter;
+import java.util.*;
 
+/**
+ * Shiro配置
+ * https://shiro.apache.org/spring.html
+ * https://shiro.apache.org/spring-boot.html
+ **/
+@Slf4j
 @Configuration
+@EnableConfigurationProperties({ShiroProperties.class})
+@ConditionalOnProperty(value = {"spring-boot-jjj.shiro.enable"}, matchIfMissing = true)
 public class ShiroConfig {
 
+    /**
+     * JWT过滤器名称
+     */
+    private static final String JWT_FILTER_NAME = "jwtFilter";
+
+    /**
+     * Shiro过滤器名称
+     */
+    private static final String SHIRO_FILTER_NAME = "shiroFilter";
+
+    /**
+     * anon
+     */
+    private static final String ANON = "anon";
+
+
     @Bean
-    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager mySecurityManager) {
-        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
-        shiroFilterFactoryBean.setSecurityManager(mySecurityManager);
-        // 登录页面
-        shiroFilterFactoryBean.setLoginUrl("/login");
-        // 未授权页面
-        shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
-        return shiroFilterFactoryBean;
+    public CredentialsMatcher credentialsMatcher() {
+        return new JwtCredentialsMatcher();
     }
 
+    /**
+     * JWT数据源验证
+     *
+     * @return
+     */
     @Bean
-    public SecurityManager mySecurityManager(Authenticator authenticator, Authorizer authorizer, SessionManager sessionManager) {
+    public JwtRealm jwtRealm() {
+        JwtRealm jwtRealm = new JwtRealm();
+//        jwtRealm.setCachingEnabled(false);
+        jwtRealm.setCredentialsMatcher(credentialsMatcher());
+        return jwtRealm;
+    }
+
+
+    @Bean
+    public SessionStorageEvaluator sessionStorageEvaluator() {
+        DefaultSessionStorageEvaluator sessionStorageEvaluator = new DefaultWebSessionStorageEvaluator();
+        sessionStorageEvaluator.setSessionStorageEnabled(false);
+        return sessionStorageEvaluator;
+    }
+
+    @Bean
+    public DefaultSubjectDAO subjectDAO() {
+        DefaultSubjectDAO defaultSubjectDAO = new DefaultSubjectDAO();
+        defaultSubjectDAO.setSessionStorageEvaluator(sessionStorageEvaluator());
+        return defaultSubjectDAO;
+    }
+
+    /**
+     * 安全管理器配置
+     *
+     * @return
+     */
+    @Bean
+    public SecurityManager securityManager() {
         DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
-        securityManager.setAuthenticator(authenticator); // 使用配置好的 Authenticator
-        securityManager.setAuthorizer(authorizer);
-        securityManager.setSessionManager(sessionManager);
+        securityManager.setRealm(jwtRealm());
+        securityManager.setSubjectDAO(subjectDAO());
+        SecurityUtils.setSecurityManager(securityManager);
         return securityManager;
     }
 
+    /**
+     * ShiroFilterFactoryBean配置
+     *
+     * @param securityManager
+     * @param shiroProperties
+     * @param jwtProperties
+     * @return
+     */
+    @Bean(SHIRO_FILTER_NAME)
+    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,
+
+                                                         ShiroProperties shiroProperties,
+                                                         JwtProperties jwtProperties) {
+        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
+        shiroFilterFactoryBean.setSecurityManager(securityManager);
+        Map<String, Filter> filterMap = getFilterMap( jwtProperties);
+        shiroFilterFactoryBean.setFilters(filterMap);
+        Map<String, String> filterChainMap = null;
+        try {
+            filterChainMap = getFilterChainDefinitionMap(shiroProperties);
+
+        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainMap);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return shiroFilterFactoryBean;
+    }
+
+
+    /**
+     * 获取filter map
+     *
+     * @return
+     */
+    private Map<String, Filter> getFilterMap(
+                                             JwtProperties jwtProperties) {
+        Map<String, Filter> filterMap = new LinkedHashMap<>();
+        filterMap.put(JWT_FILTER_NAME, new JwtFilter(jwtProperties));
+        return filterMap;
+    }
+
+
+    /**
+     * Shiro路径权限配置
+     *
+     * @return
+     */
+    private Map<String, String> getFilterChainDefinitionMap(ShiroProperties shiroProperties) throws Exception {
+        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
+        // 获取排除的路径
+        List<String[]> anonList = shiroProperties.getAnon();
+        log.debug("anonList:{}", JSON.toJSONString(anonList));
+        if (CollectionUtils.isNotEmpty(anonList)) {
+            anonList.forEach(anonArray -> {
+                if (ArrayUtils.isNotEmpty(anonArray)) {
+                    for (String anonPath : anonArray) {
+                        filterChainDefinitionMap.put(anonPath, ANON);
+                    }
+                }
+            });
+        }
+
+        // 获取ini格式配置
+        String definitions = shiroProperties.getFilterChainDefinitions();
+        if (StringUtils.isNotBlank(definitions)) {
+            Map<String, String> section = IniUtil.parseIni(definitions);
+            log.debug("definitions:{}", JSON.toJSONString(section));
+            for (Map.Entry<String, String> entry : section.entrySet()) {
+                filterChainDefinitionMap.put(entry.getKey(), entry.getValue());
+            }
+        }
+
+        // 获取自定义权限路径配置集合
+        List<ShiroPermissionProperties> permissionConfigs = shiroProperties.getPermission();
+        log.debug("permissionConfigs:{}", JSON.toJSONString(permissionConfigs));
+        if (CollectionUtils.isNotEmpty(permissionConfigs)) {
+            for (ShiroPermissionProperties permissionConfig : permissionConfigs) {
+                String url = permissionConfig.getUrl();
+                String[] urls = permissionConfig.getUrls();
+                String permission = permissionConfig.getPermission();
+                if (StringUtils.isBlank(url) && ArrayUtils.isEmpty(urls)) {
+                    throw new Exception("shiro permission config 路径配置不能为空");
+                }
+                if (StringUtils.isBlank(permission)) {
+                    throw new Exception("shiro permission config permission不能为空");
+                }
+
+                if (StringUtils.isNotBlank(url)) {
+                    filterChainDefinitionMap.put(url, permission);
+                }
+                if (ArrayUtils.isNotEmpty(urls)) {
+                    for (String string : urls) {
+                        filterChainDefinitionMap.put(string, permission);
+                    }
+                }
+            }
+        }
+
+        // 如果启用shiro,则设置最后一个设置为JWTFilter,否则全部路径放行
+        if (shiroProperties.isEnable()) {
+            filterChainDefinitionMap.put("/**", JWT_FILTER_NAME);
+        } else {
+            filterChainDefinitionMap.put("/**", ANON);
+        }
+
+        log.debug("filterChainMap:{}", JSON.toJSONString(filterChainDefinitionMap));
+
+        // 添加默认的filter
+        Map<String, String> newFilterChainDefinitionMap = addDefaultFilterDefinition(filterChainDefinitionMap);
+        return newFilterChainDefinitionMap;
+    }
+
+    /**
+     * 添加默认的filter权限过滤
+     *
+     * @param filterChainDefinitionMap
+     * @return
+     */
+    private Map<String, String> addDefaultFilterDefinition(Map<String, String> filterChainDefinitionMap) {
+        if (MapUtils.isEmpty(filterChainDefinitionMap)) {
+            return filterChainDefinitionMap;
+        }
+        final Map<String, String> map = new LinkedHashMap<>();
+        for (Map.Entry<String, String> entry : filterChainDefinitionMap.entrySet()) {
+            String key = entry.getKey();
+            String value = entry.getValue();
+            String definition;
+            String[] strings = value.split(",");
+            List<String> list = new ArrayList<>();
+            list.addAll(Arrays.asList(strings));
+            definition = String.join(",", list);
+            map.put(key, definition);
+        }
+        return map;
+    }
+
+    /**
+     * ShiroFilter配置
+     *
+     * @return
+     */
     @Bean
-    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
-        DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
-        // 配置哪些请求需要受保护,以及访问这些请求需要的权限
-        chainDefinition.addPathDefinition("/login", "anon"); // 登录接口可匿名访问
-        chainDefinition.addPathDefinition("/unauthorized", "anon"); // 未授权页面可匿名访问
-        chainDefinition.addPathDefinition("/**", "authc"); // 其他请求需要认证
-        return chainDefinition;
+    public FilterRegistrationBean delegatingFilterProxy() {
+        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
+        DelegatingFilterProxy proxy = new DelegatingFilterProxy();
+        proxy.setTargetFilterLifecycle(true);
+        proxy.setTargetBeanName(SHIRO_FILTER_NAME);
+        filterRegistrationBean.setFilter(proxy);
+        filterRegistrationBean.setAsyncSupported(true);
+        filterRegistrationBean.setEnabled(true);
+        filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);
+        return filterRegistrationBean;
     }
 
     @Bean
-    public Authenticator authenticator(MyRealm myRealm) { // 注入 MyRealm
+    public Authenticator authenticator() {
         ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator();
-        // 将 MyRealm 添加到 Authenticator 的 realms 列表中
-        authenticator.setRealms(Collections.singletonList(myRealm));
+        authenticator.setRealms(Arrays.asList(jwtRealm()));
+        authenticator.setAuthenticationStrategy(new FirstSuccessfulStrategy());
         return authenticator;
     }
 
+
+    /**
+     * Enabling Shiro Annotations
+     *
+     * @return
+     */
     @Bean
-    public Authorizer authorizer() {
-        return new ModularRealmAuthorizer();
+    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
+        return new LifecycleBeanPostProcessor();
     }
 
     @Bean
-    public SessionManager sessionManager() {
-        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
-        // 这里可以进行更多的 sessionManager 配置,例如设置 sessionDAO
-        SessionDAO sessionDAO = new MemorySessionDAO();
-        sessionManager.setSessionDAO(sessionDAO);
-        return sessionManager;
+    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
+        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
+        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
+        return authorizationAttributeSourceAdvisor;
     }
-}
+
+}

+ 80 - 0
src/main/java/com/zhentao/shiro/utils/JwtToken.java

@@ -0,0 +1,80 @@
+
+
+package com.zhentao.shiro.utils;
+
+import com.auth0.jwt.interfaces.DecodedJWT;
+
+import com.zhentao.util.IpUtil;
+import lombok.Data;
+import lombok.experimental.Accessors;
+import org.apache.shiro.authc.HostAuthenticationToken;
+
+import java.util.Date;
+
+/**
+ * Shiro JwtToken对象
+ **/
+@Data
+@Accessors(chain = true)
+public class JwtToken implements HostAuthenticationToken {
+	private static final long serialVersionUID = 5101247566043093405L;
+	
+	/**
+     * 登录ip
+     */
+    private String host;
+    /**
+     * 登录用户名称
+     */
+    private String username;
+    /**
+     * 登录盐值
+     */
+    private String salt;
+    /**
+     * 登录token
+     */
+    private String token;
+    /**
+     * 创建时间
+     */
+    private Date createDate;
+    /**
+     * 多长时间过期,默认一小时
+     */
+    private long expireSecond;
+    /**
+     * 过期日期
+     */
+    private Date expireDate;
+
+    private String principal;
+
+    private String credentials;
+
+    @Override
+    public Object getPrincipal() {
+        return token;
+    }
+
+    @Override
+    public Object getCredentials() {
+        return token;
+    }
+
+    public static JwtToken build(String token, String username, String salt, long expireSecond) {
+        DecodedJWT decodedJwt = JwtUtil.getJwtInfo(token);
+        Date createDate = decodedJwt.getIssuedAt();
+        Date expireDate = decodedJwt.getExpiresAt();
+        return new JwtToken()
+                .setUsername(username)
+                .setToken(token)
+                .setHost(IpUtil.getRequestIp())
+                .setSalt(salt)
+                .setCreateDate(createDate)
+                .setExpireSecond(expireSecond)
+                .setExpireDate(expireDate);
+
+    }
+
+}

+ 88 - 0
src/main/java/com/zhentao/shiro/utils/JwtTokenUtil.java

@@ -0,0 +1,88 @@
+
+
+package com.zhentao.shiro.utils;
+
+
+import com.zhentao.properties.JwtProperties;
+import com.zhentao.util.HttpServletRequestUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * JwtToken工具类
+ **/
+@Slf4j
+@Component
+public class  JwtTokenUtil {
+
+    private static String tokenName;
+
+    public JwtTokenUtil(JwtProperties jwtProperties) {
+        tokenName = jwtProperties.getTokenName();
+        log.debug("tokenName:{}", tokenName);
+    }
+
+    /**
+     * 获取token名称
+     *
+     * @return
+     */
+    public static String getTokenName(String model) {
+        if(model.equals("")){
+            return tokenName;
+        }else{
+            return tokenName + "" + model;
+        }
+    }
+
+    /**
+     * 从请求头或者请求参数中
+     *
+     * @return
+     */
+    public static String getToken(String model) {
+        return getToken(HttpServletRequestUtil.getRequest(), model);
+    }
+
+    public static String getModel(String path){
+        String model = "";
+        if(path.startsWith("/api/admin/")){
+            model = "admin";
+        }else if(path.startsWith("/api/shop/")){
+            model = "shop";
+        }else if(path.startsWith("/api/supplier/")) {
+            model = "supplier";
+        }else if(path.startsWith("/api/service/")){
+            model = "service";
+        }
+        return model;
+    }
+
+    /**
+     * 从请求头或者请求参数中
+     *
+     * @param request
+     * @return
+     */
+    public static String getToken(HttpServletRequest request, String model) {
+        if (request == null) {
+            throw new IllegalArgumentException("request不能为空");
+        }
+        String realTokenName = "";
+        if(model.equals("")){
+            realTokenName = tokenName;
+        }else{
+            realTokenName = tokenName + "" + model;
+        }
+        // 从请求头中获取token
+        String token = request.getHeader(realTokenName);
+        if (StringUtils.isBlank(token)) {
+            // 从请求参数中获取token
+            token = request.getParameter(realTokenName);
+        }
+        return token;
+    }
+}

+ 186 - 0
src/main/java/com/zhentao/shiro/utils/JwtUtil.java

@@ -0,0 +1,186 @@
+
+
+package com.zhentao.shiro.utils;
+
+import com.alibaba.fastjson.JSON;
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.JWTVerifier;
+import com.auth0.jwt.algorithms.Algorithm;
+import com.auth0.jwt.interfaces.DecodedJWT;
+import com.zhentao.constants.CommonConstant;
+import com.zhentao.properties.JwtProperties;
+import com.zhentao.util.UUIDUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.time.DateUtils;
+import org.springframework.stereotype.Component;
+
+import java.time.Duration;
+import java.util.Date;
+
+/**
+ * JWT工具类
+ * https://github.com/auth0/java-jwt
+ **/
+@Slf4j
+@Component
+public class JwtUtil {
+
+    private static JwtProperties jwtProperties;
+
+    public JwtUtil(JwtProperties jwtProperties) {
+        JwtUtil.jwtProperties = jwtProperties;
+        log.info(JSON.toJSONString(JwtUtil.jwtProperties));
+    }
+
+    /**
+     * 生成JWT Token
+     *
+     * @param username       用户名
+     * @param salt           盐值
+     * @param expireDuration 过期时间和单位
+     * @return token
+     */
+    public static String generateToken(String username, String salt, Duration expireDuration) {
+        try {
+            if (StringUtils.isBlank(username)) {
+                log.error("username不能为空");
+                return null;
+            }
+            log.debug("username:{}", username);
+
+            // 如果盐值为空,则使用默认值:888888
+            if (StringUtils.isBlank(salt)) {
+                salt = jwtProperties.getSecret();
+            }
+            log.debug("salt:{}", salt);
+
+            // 过期时间,单位:秒
+            Long expireSecond;
+            // 默认过期时间为1小时
+            if (expireDuration == null) {
+                expireSecond = jwtProperties.getExpireSecond();
+            } else {
+                expireSecond = expireDuration.getSeconds();
+            }
+            log.debug("expireSecond:{}", expireSecond);
+            Date expireDate = DateUtils.addSeconds(new Date(), expireSecond.intValue());
+            log.debug("expireDate:{}", expireDate);
+
+            // 生成token
+            Algorithm algorithm = Algorithm.HMAC256(salt);
+            String token = JWT.create()
+                    .withClaim(CommonConstant.JWT_USERNAME, username)
+                    // jwt唯一id
+                    .withJWTId(UUIDUtil.getUuid())
+                    // 签发人
+                    .withIssuer(jwtProperties.getIssuer())
+                    // 主题
+                    .withSubject(jwtProperties.getSubject())
+                    // 签发的目标
+                    .withAudience(jwtProperties.getAudience())
+                    // 签名时间
+                    .withIssuedAt(new Date())
+                    // token过期时间
+                    .withExpiresAt(expireDate)
+                    // 签名
+                    .sign(algorithm);
+            return token;
+        } catch (Exception e) {
+            log.error("generateToken exception", e);
+        }
+        return null;
+    }
+
+    public static boolean verifyToken(String token, String salt) {
+        try {
+            Algorithm algorithm = Algorithm.HMAC256(salt);
+            JWTVerifier verifier = JWT.require(algorithm)
+                    // 签发人
+                    .withIssuer(jwtProperties.getIssuer())
+                    // 主题
+                    .withSubject(jwtProperties.getSubject())
+                    // 签发的目标
+                    .withAudience(jwtProperties.getAudience())
+                    .build();
+            DecodedJWT jwt = verifier.verify(token);
+            if (jwt != null) {
+                return true;
+            }
+        } catch (Exception e) {
+            log.error("Verify Token Exception", e);
+        }
+        return false;
+    }
+
+    /**
+     * 解析token,获取token数据
+     *
+     * @param token
+     * @return
+     */
+    public static DecodedJWT getJwtInfo(String token) {
+        return JWT.decode(token);
+    }
+
+    /**
+     * 获取用户名
+     *
+     * @param token
+     * @return
+     */
+    public static String getUsername(String token) {
+        if (StringUtils.isBlank(token)){
+            return null;
+        }
+        DecodedJWT decodedJwt = getJwtInfo(token);
+        if (decodedJwt == null) {
+            return null;
+        }
+        String username = decodedJwt.getClaim(CommonConstant.JWT_USERNAME).asString();
+        return username;
+    }
+
+    /**
+     * 获取创建时间
+     *
+     * @param token
+     * @return
+     */
+    public static Date getIssuedAt(String token) {
+        DecodedJWT decodedJwt = getJwtInfo(token);
+        if (decodedJwt == null) {
+            return null;
+        }
+        return decodedJwt.getIssuedAt();
+    }
+
+    /**
+     * 获取过期时间
+     *
+     * @param token
+     * @return
+     */
+    public static Date getExpireDate(String token) {
+        DecodedJWT decodedJwt = getJwtInfo(token);
+        if (decodedJwt == null) {
+            return null;
+        }
+        return decodedJwt.getExpiresAt();
+    }
+
+    /**
+     * 判断token是否已过期
+     *
+     * @param token
+     * @return
+     */
+    public static boolean isExpired(String token) {
+        Date expireDate = getExpireDate(token);
+        if (expireDate == null) {
+            return true;
+        }
+        return expireDate.before(new Date());
+    }
+
+}

+ 60 - 0
src/main/java/com/zhentao/shiro/utils/SaltUtil.java

@@ -0,0 +1,60 @@
+
+
+package com.zhentao.shiro.utils;
+
+
+import com.zhentao.properties.JwtProperties;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shiro.crypto.SecureRandomNumberGenerator;
+
+/**
+ * 盐值包装工具类
+ **/
+public class SaltUtil {
+
+    /**
+     * 盐值包装
+     *
+     * @param secret 配置文件中配置的附加盐值
+     * @param salt   数据库中保存的盐值
+     * @return
+     */
+    public static String getSalt(String secret, String salt) {
+        if (StringUtils.isBlank(secret) && StringUtils.isBlank(salt)) {
+            return null;
+        }
+        // 加密方法
+        String newSalt = DigestUtils.sha256Hex(secret + salt);
+        return newSalt;
+    }
+
+    /**
+     * 生成6位随机盐
+     *
+     * @return
+     */
+    public static String generateSalt() {
+        return new SecureRandomNumberGenerator().nextBytes(3).toHex();
+    }
+
+    /**
+     * 加工盐值
+     *
+     * @param salt
+     * @param jwtProperties
+     * @return
+     */
+    public static String getSalt(String salt, JwtProperties jwtProperties) {
+        String newSalt;
+        if (jwtProperties.isSaltCheck()) {
+            // 包装盐值
+            newSalt = SaltUtil.getSalt(jwtProperties.getSecret(), salt);
+        } else {
+            newSalt = jwtProperties.getSecret();
+        }
+        return newSalt;
+    }
+
+}
+

+ 18 - 0
src/main/java/com/zhentao/util/HttpServletRequestUtil.java

@@ -0,0 +1,18 @@
+
+
+package com.zhentao.util;
+
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * 获取当前请求的HttpServletRequest对象
+ */
+public class HttpServletRequestUtil {
+
+    public static HttpServletRequest getRequest() {
+        return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
+    }
+}

+ 42 - 0
src/main/java/com/zhentao/util/IniUtil.java

@@ -0,0 +1,42 @@
+
+
+package com.zhentao.util;
+
+import org.ini4j.Config;
+import org.ini4j.Ini;
+import org.ini4j.Profile;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.Map;
+
+public class IniUtil {
+    public static Map<String,String> parseIni(String string) {
+        Config config = new Config();
+        config.setGlobalSection(true);
+        config.setGlobalSectionName("");
+        Ini ini = new Ini();
+        ini.setConfig(config);
+        try {
+            ini.load(new StringReader(string));
+            Profile.Section section = ini.get("");
+            return section;
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    public static Map<String,String> parseIni(String sectionName,String string) {
+        Ini ini = new Ini();
+        try {
+            ini.load(new StringReader(string));
+            Profile.Section section = ini.get(sectionName);
+            return section;
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+}

+ 65 - 0
src/main/java/com/zhentao/util/IpUtil.java

@@ -0,0 +1,65 @@
+
+
+package com.zhentao.util;
+
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import javax.servlet.http.HttpServletRequest;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/**
+ * 获取IP地址工具类
+ */
+public final class IpUtil {
+
+    private static final String UNKNOWN = "unknown";
+    private static final String IPV6_LOCAL = "0:0:0:0:0:0:0:1";
+
+    private IpUtil(){
+        throw new AssertionError();
+    }
+
+    /**
+     * 获取请求用户的IP地址
+     * @return
+     */
+    public static String getRequestIp() {
+        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+        HttpServletRequest request = attributes.getRequest();
+        return getRequestIp(request);
+    }
+
+    /**
+     * 获取请求用户的IP地址
+     * @param request
+     * @return
+     */
+    public static String getRequestIp(HttpServletRequest request) {
+        String ip = request.getHeader("x-forwarded-for");
+        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
+            ip = request.getHeader("Proxy-Client-IP");
+        }
+        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
+            ip = request.getHeader("WL-Proxy-Client-IP");
+        }
+        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
+            ip = request.getRemoteAddr();
+        }
+
+        if (IPV6_LOCAL.equals(ip)){
+            ip = getLocalhostIp();
+        }
+        return ip;
+    }
+
+    public static String getLocalhostIp(){
+        try {
+            return InetAddress.getLocalHost().getHostAddress();
+        } catch (UnknownHostException e) {
+        }
+        return null;
+    }
+
+}

+ 14 - 0
src/main/java/com/zhentao/util/UUIDUtil.java

@@ -0,0 +1,14 @@
+
+
+package com.zhentao.util;
+
+import java.util.UUID;
+
+public class UUIDUtil {
+
+    public static String getUuid(){
+        String uuid = UUID.randomUUID().toString().replaceAll("-", "");
+        return uuid;
+    }
+    
+}

+ 54 - 2
src/main/resources/application.yml

@@ -1,8 +1,60 @@
+# 应用服务 WEB 访问端口
 server:
   port: 9527
+springboot-zhentao:
+  jwt:
+    # token请求头名称
+    token-name: token
+    # jwt密钥
+    secret: 888888
+    # 发行人
+    issuer: zhentao
+    # 观众
+    audience: web
+    # 默认过期时间1小时,单位:秒
+    expire-second: 36000
+    # 是否刷新token
+    refresh-token: true
+    # 刷新token的时间间隔,默认10分钟,单位:秒
+    refresh-token-countdown: 600
+    # redis校验jwt token是否存在,可选
+    redis-check: true
+    # true: 同一个账号只能是最后一次登录token有效,false:同一个账号可多次登录
+    single-login: false
+    # 盐值校验,如果不加自定义盐值,则使用secret校验
+    salt-check: true
+
+  shiro:
+    # 是否启用
+    enable: true
+    # 权限配置
+    anon:
+      # 排除静态资源
+      - /static/**,/templates/**
+      # 排除Swagger
+      # 排除actuator
+      - /actuator/**
+      - # 排除首页
+      - /,/index.html
+      # front模块
+      - /front/**
+      # admin模块
+      - /admin/passport/login
+
+      - # service模块
+      - /login
+      # job模块
+      - /job/**
+    # 多行字符串权限配置
+    filter-chain-definitions: |
+      /resource/**=anon
+      /uploads/**=anon
+      /verificationCode/**=anon
+      /enum=anon
+      /getSysUserInfo=anon
 spring:
   datasource:
     driver-class-name: com.mysql.cj.jdbc.Driver
-    url: jdbc:mysql://127.0.0.1:3306/goose?serverTimezone=UTC
+    url: jdbc:mysql://47.111.130.63:3306/goose_course?serverTimezone=UTC
     username: root
-    password: hch030923
+    password: root

+ 3 - 3
src/main/resources/templates/home.html

@@ -1,10 +1,10 @@
 <!DOCTYPE html>
 <html xmlns:th="http://www.thymeleaf.org">
 <head>
-    <title>Home</title>
+    <title>主页</title>
 </head>
 <body>
-<h1>Welcome to the Home Page!</h1>
-<a href="/logout">Logout</a>
+<h1>欢迎,<span th:text="${username}"></span>!</h1>
+<a href="/logout">退出登录</a>
 </body>
 </html>

+ 7 - 9
src/main/resources/templates/login.html

@@ -1,17 +1,15 @@
 <!DOCTYPE html>
 <html xmlns:th="http://www.thymeleaf.org">
 <head>
-    <title>Login</title>
+    <title>登录</title>
 </head>
 <body>
-<h1>Login</h1>
-<form action="/login" method="post">
-    <label for="username">Username:</label>
-    <input type="text" id="username" name="username" required><br>
-    <label for="password">Password:</label>
-    <input type="password" id="password" name="password" required><br>
-    <button type="submit">Login</button>
+<h1>用户登录</h1>
+<div th:if="${error}" style="color: red;" th:text="${error}"></div>
+<form method="post">
+    用户名:<input type="text" name="username" required><br>
+    密码:<input type="password" name="password" required><br>
+    <button type="submit">登录</button>
 </form>
-<p th:if="${param.error}" th:text="${param.error}">Error message</p>
 </body>
 </html>

+ 3 - 3
src/main/resources/templates/unauthorized.html

@@ -1,9 +1,9 @@
 <!DOCTYPE html>
-<html xmlns:th="http://www.thymeleaf.org">
+<html>
 <head>
-    <title>Unauthorized</title>
+    <title>权限不足</title>
 </head>
 <body>
-<h1>You are not authorized to access this page.</h1>
+<h1>你没有权限访问此页面!</h1>
 </body>
 </html>

+ 0 - 47
src/test/java/com/zhentao/ShiroIntegrationTest.java

@@ -1,47 +0,0 @@
-package com.zhentao;
-
-import org.junit.jupiter.api.Test;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.http.MediaType;
-import org.springframework.test.web.servlet.MockMvc;
-
-import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
-import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
-
-@SpringBootTest
-@AutoConfigureMockMvc
-public class ShiroIntegrationTest {
-
-    @Autowired
-    private MockMvc mockMvc;
-
-    @Test
-    public void testLoginSuccess() throws Exception {
-        mockMvc.perform(post("/login")
-                        .param("username", "admin")
-                        .param("password", "123456")
-                        .contentType(MediaType.APPLICATION_FORM_URLENCODED))
-                .andExpect(status().is3xxRedirection())
-                .andExpect(redirectedUrl("/home"));
-    }
-
-    @Test
-    public void testLoginFailure() throws Exception {
-        mockMvc.perform(post("/login")
-                        .param("username", "unknown")
-                        .param("password", "123")
-                        .contentType(MediaType.APPLICATION_FORM_URLENCODED))
-                .andExpect(status().is3xxRedirection())
-                .andExpect(redirectedUrl("/login?error=invalid"));
-    }
-
-    @Test
-    public void testAccessProtectedResourceWithoutLogin() throws Exception {
-        mockMvc.perform(get("/home"))
-                .andExpect(status().is3xxRedirection())
-                .andExpect(redirectedUrl("/login"));
-    }
-}