前后端分离无非就是前端只是写前端,后台专注接口,交互就使用token进行交互。

源码地址
欢迎star

1 实现AuthenticationToken接口,重写token

AuthToken:

package com.luoyuanxiangvip.admin.core.shiro;

import org.apache.shiro.authc.AuthenticationToken;

/**
 * <p>
 * 自定义token
 * </p>
 *
 * @author luoyuanxiang <p>luoyuanxiangvip.com</p>
 * @since 2019/5/26 18:49
 */
public class AuthToken implements AuthenticationToken {

    private static final long serialVersionUID = 2424386775104576139L;
    /** 自定义 token */
    private String token;

    public AuthToken(String token) {
        this.token = token;
    }

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

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

2 创建生成token工具

导入jwt工具包:

<dependency>
   <groupId>io.jsonwebtoken</groupId>
   <artifactId>jjwt</artifactId>
   <version>0.9.0</version>
</dependency>

创建JwtPropertiesJwtToken类用于生成token
JwtProperties

package com.luoyuanxiangvip.admin.core.jwt;

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * <p>
 * jwt配置
 * </p>
 *
 * @author luoyuanxiang <p>luoyuanxiangvip.com</p>
 * @since 2019/5/26 18:53
 */
@Getter
@Setter
@Component
@ConfigurationProperties("jwt")
public class JwtProperties {

    private String header;

    private String secret;

    private Long expiration;

    private String md5Key;
}

JwtToken

package com.luoyuanxiangvip.admin.core.jwt;

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

import javax.annotation.Resource;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * <p>
 * 自定义token
 * </p>
 *
 * @author luoyuanxiang <p>luoyuanxiangvip.com</p>
 * @since 2019/5/26 18:53
 */
@Component
public class JwtToken {

    @Resource
    private JwtProperties jwtProperties;

    /**
     * 获取用户名从token中
     * @param token token
     * @return 主题或用户名
     */
    public String getUsernameFromToken(String token) {
        try {
            return getClaimFromToken(token).getSubject();
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * 获取jwt发布时间
     * @param token token
     * @return 时间
     */
    public Date getIssuedAtDateFromToken(String token) {
        try {
            return getClaimFromToken(token).getIssuedAt();
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * 获取jwt失效时间
     * @param token token
     * @return 时间
     */
    public Date getExpirationDateFromToken(String token){
        try {
            return getClaimFromToken(token).getExpiration();
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * 获取jwt接收者
     * @param token token
     * @return string
     */
    public String getAudienceFromToken(String token){
        try {
            return getClaimFromToken(token).getAudience();
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * 获取私有的jwt claim
     * @param token token
     * @param key key
     * @return String
     */
    private String getPrivateClaimFromToken(String token, String key){
        try {
            return getClaimFromToken(token).get(key).toString();
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * 获取md5 key从token中
     * @param token token
     * @return string
     */
    public String getMd5KeyFromToken(String token){
        return getPrivateClaimFromToken(token, jwtProperties.getMd5Key());
    }

    /**
     * 获取jwt的payload部分
     * @param token token
     * @return Claims
     */
    private Claims getClaimFromToken(String token) throws Exception {
        return Jwts.parser()
                .setSigningKey(jwtProperties.getSecret())
                .parseClaimsJws(token)
                .getBody();
    }

    /**
     * 解析token是否正确,不正确会报异常
     * @param token token
     */
    public boolean parseToken(String token){
        Claims body = null;
        try {
            body = getClaimFromToken(token);
        } catch (Exception e) {
            return true;
        }
        return body.size() <= 0;
    }

    /**
     * 验证token是否失效
     * true:过期   false:没过期
     * @param token token
     * @return Boolean
     */
    public Boolean isTokenExpired(String token){
        final Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }

    /**
     * 生成token(通过用户名和签名时候用的随机数)
     * @param userName 用户名
     * @param randomKey 用户密码或key
     * @return token
     */
    public String generateToken(String userName, String randomKey) {
        Map<String, Object> claims = new HashMap<>(1);
        claims.put(jwtProperties.getMd5Key(), randomKey);
        return doGenerateToken(claims, userName);
    }

    /**
     * token 刷新
     * @param token token
     * @return new token
     */
    public String refreshToken(String token) throws Exception {
        final Date createdDate = new Date();
        final Date expirationDate = new Date(createdDate.getTime() + jwtProperties.getExpiration() * 1000);
        String subject = this.getUsernameFromToken(token);
        Claims claims = this.getClaimFromToken(token);
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setIssuedAt(createdDate)
                .setExpiration(expirationDate)
                .signWith(SignatureAlgorithm.HS512, jwtProperties.getSecret())
                .compact();
    }

    /**
     * 生成token
     * @param claims 需要加密的用户信息
     * @param subject 主题
     * @return token
     */
    private String doGenerateToken(Map<String, Object> claims, String subject) {
        final Date createdDate = new Date();
        final Date expirationDate = new Date(createdDate.getTime() + jwtProperties.getExpiration() * 1000);
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setIssuedAt(createdDate)
                .setExpiration(expirationDate)
                .signWith(SignatureAlgorithm.HS512, jwtProperties.getSecret())
                .compact();
    }
}

application-dev.yml

jwt:
  #请求头
  header: Authorization
  secret: defaultSecret
  # token 过期时间 单位秒 7天
  expiration: 604800
  # md5key
  md5Key: randomKey

3 创建拦截器对token进行拦截处理

JwtFilter 判断是否存在token

package com.luoyuanxiangvip.admin.core.filter;

import com.luoyuanxiangvip.admin.core.shiro.AuthToken;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

/**
 * <p>
 * jwtfilter
 * </p>
 *
 * @author luoyuanxiang <p>luoyuanxiangvip.com</p>
 * @since 2019/5/27 9:50
 */
public class JwtFilter extends BasicHttpAuthenticationFilter {
    /**
     * 判断用户是否想要登入。
     * 检测header里面是否包含Authorization字段即可
     */
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        String authorization = getAuthzHeader(request);
        return authorization != null;
    }

    /**
     * 创建自定义token
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        String authorization = getAuthzHeader(request);
        AuthToken token = new AuthToken(authorization);
        // 提交给realm进行登入,如果错误他会抛出异常并被捕获
        getSubject(request, response).login(token);
        // 如果没有抛出异常则代表登入成功,返回true
        return true;
    }

    /**
     * 这里我们详细说明下为什么最终返回的都是true,即允许访问
     * 例如我们提供一个地址 GET /article
     * 登入用户和游客看到的内容是不同的
     * 如果在这里返回了false,请求会被直接拦截,用户看不到任何东西
     * 所以我们在这里返回true,Controller中可以通过 subject.isAuthenticated() 来判断用户是否登入
     * 如果有些资源只有登入用户才能访问,我们只需要在方法上面加上 @RequiresAuthentication 注解即可
     * 但是这样做有一个缺点,就是不能够对GET,POST等请求进行分别过滤鉴权(因为我们重写了官方的方法),但实际上对应用影响不大
     * 每一个方法都需要添加 @RequiresAuthentication 是因为我们重写了 token 全局异常捕获不到 所以需要 @RequiresAuthentication 来进行验证捕获异常
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        if (isLoginAttempt(request, response)) {
            try {
                executeLogin(request, response);
            } catch (Exception ignored) {}
        }
        return true;
    }
}

AccessFilter

package com.luoyuanxiangvip.admin.core.filter;

import com.alibaba.fastjson.JSONObject;
import com.luoyuanxiangvip.core.utils.AjaxResult;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.web.filter.AccessControlFilter;

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

/**
 * <p>
 * 是否携带token访问
 * </p>
 *
 * @author luoyuanxiang <p>luoyuanxiangvip.com</p>
 * @since 2019/5/27 9:49
 */
public class AccessFilter extends AccessControlFilter {

    private static final String AUTHORIZATION_HEADER = "Authorization";


    /**
     * 可以发现他是调用的isAccessAllowed方法和onAccessDenied方法,只要两者有一个可以就可以了,
     * 从名字中我们也可以理解,他的逻辑是这样:
     * 先调用isAccessAllowed,如果返回的是true,
     * 则直接放行执行后面的filter和servlet,如果返回的是false,
     * 则继续执行后面的onAccessDenied方法,
     * 如果后面返回的是true则也可以有权限继续执行后面的filter和servelt。
     * 只有两个函数都返回false才会阻止后面的filter和servlet的执行。
     * @param request request
     * @param response response
     * @param mappedValue mappedValue
     * @return boolean
     * @throws Exception Exception
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        HttpServletRequest req = (HttpServletRequest) request;
        String authorization = req.getHeader(AUTHORIZATION_HEADER);
        // 如果没有token 那么就不能访问资源 返回false 后面的调用方法不执行
        return StringUtils.isNotEmpty(authorization);
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;
        res.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
        res.setHeader("Access-Control-Allow-Credentials","true");
        res.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,PATCH,OPTIONS");
        res.setHeader("Access-Control-Allow-Headers", "Content-Type, Accept, Authorization");
        res.setCharacterEncoding("UTF-8");
        res.getWriter().println(JSONObject.toJSONString(AjaxResult.error(50008,"token无效")));
        return false;
    }
}

4 创建ShiroConfig

package com.luoyuanxiangvip.admin.core.config;

import com.luoyuanxiangvip.admin.core.filter.AccessFilter;
import com.luoyuanxiangvip.admin.core.filter.JwtFilter;
import com.luoyuanxiangvip.admin.core.shiro.UserRealm;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
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.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;

import javax.servlet.Filter;
import java.util.HashMap;
import java.util.Map;

/**
 * <p>
 * shiro配置
 * </p>
 *
 * @author luoyuanxiang <p>luoyuanxiangvip.com</p>
 * @since 2019/5/26 18:47
 */
@Configuration
public class ShiroConfig {

    @Bean("securityManager")
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        // 使用自己的realm
        manager.setRealm(userRealm());
        /*
         * 关闭shiro自带的session,详情见文档
         * http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
         */
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        manager.setSubjectDAO(subjectDAO);

        return manager;
    }

    @Bean
    public UserRealm userRealm() {
        return new UserRealm();
    }

    @Bean("shiroFilter")
    public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();

        // 添加自己的过滤器并且取名为jwt
        Map<String, Filter> filterMap = new HashMap<>();
        filterMap.put("jwt", new JwtFilter());
        filterMap.put("accessFilter",new AccessFilter());
        factoryBean.setFilters(filterMap);
        factoryBean.setSecurityManager(securityManager);
        /*
         * 自定义url规则
         * http://shiro.apache.org/web.html#urls-
         */
        Map<String, String> filterRuleMap = new HashMap<>();
        filterRuleMap.put("/login","anon");
        filterRuleMap.put("/logout","anon");
        filterRuleMap.put("/swagger-ui.html","anon");
        filterRuleMap.put("/v2/**","anon");
        filterRuleMap.put("/webjars/**", "anon");
        filterRuleMap.put("/swagger-resources/**", "anon");
        filterRuleMap.put("/swagger/**","anon");
        filterRuleMap.put("/druid/**","anon");
        filterRuleMap.put("/editor.md/**","anon");
        filterRuleMap.put("/admin/article/update1", "anon");
        // 所有请求通过我们自己的JWT Filter
        filterRuleMap.put("/**", "jwt,accessFilter");
        factoryBean.setFilterChainDefinitionMap(filterRuleMap);
        return factoryBean;
    }

    /**
     * 下面的代码是添加注解支持
     */
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        // 强制使用cglib,防止重复代理和可能引起代理出错的问题
        // https://zhuanlan.zhihu.com/p/29161098
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

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

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

5 创建认证类

UserRealm

package com.luoyuanxiangvip.admin.core.shiro;

import com.luoyuanxiangvip.admin.core.exception.ExpiredException;
import com.luoyuanxiangvip.admin.core.exception.InvalidException;
import com.luoyuanxiangvip.admin.core.exception.PasswordException;
import com.luoyuanxiangvip.admin.core.jwt.JwtToken;
import com.luoyuanxiangvip.core.constant.Constant;
import com.luoyuanxiangvip.core.entity.SysUser;
import com.luoyuanxiangvip.core.service.ISysUserService;
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 javax.annotation.Resource;
import javax.servlet.http.HttpSession;

/**
 * <p>
 * 用户认证
 * </p>
 *
 * @author luoyuanxiang <p>luoyuanxiangvip.com</p>
 * @since 2019/5/26 18:49
 */
public class UserRealm extends AuthorizingRealm {

    @Resource
    private JwtToken jwtToken;
    @Resource
    private ISysUserService iSysUserService;
    @Resource
    private HttpSession session;

    /**
     * 大坑!,必须重写此方法,不然Shiro会报错
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof AuthToken;
    }

    /**
     * 权限认证
     *
     * @param principals pr
     * @return auth
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return new SimpleAuthorizationInfo();
    }

    /**
     * 登陆认证
     *
     * @param authenticationToken token
     * @return auth
     * @throws AuthenticationException auth
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String token = (String) authenticationToken.getCredentials();
        if (jwtToken.parseToken(token)) {
            throw new AuthenticationException(new InvalidException("token无效!"));
        }
        String userName = jwtToken.getUsernameFromToken(token);
        SysUser byUserName = iSysUserService.findByUserName(userName);
        if (byUserName == null) {
            throw new AuthenticationException("用户不存在!");
        }
        if (!byUserName.getPassword().equals(jwtToken.getMd5KeyFromToken(token))) {
            throw new AuthenticationException(new PasswordException("密码错误!"));
        }
        if (jwtToken.isTokenExpired(token)) {
            throw new AuthenticationException(new ExpiredException("token过期!"));
        }
        session.setAttribute(Constant.USER, byUserName);
        return new SimpleAuthenticationInfo(token, token, getName());
    }
}

6 提供登陆接口

@ApiOperation(value = "用户登陆")
    @Logs
    @PostMapping("/login")
    public AjaxResult login(@Valid @RequestBody LoginVo loginVo) throws AuthenticationException {
        String encryptionPassword = Sha256Util.sha256(loginVo.getPassword(), loginVo.getUsername());
        String accessToken = jwtToken.generateToken(loginVo.getUsername(), encryptionPassword);
        AuthToken authToken = new AuthToken(accessToken);
        Subject subject = SecurityUtils.getSubject();
        subject.login(authToken);
        if (subject.isAuthenticated()) {
            JwtVo jwtVo = new JwtVo();
            long accessExpiration = jwtToken.getExpirationDateFromToken(accessToken).getTime();
            long createTime = jwtToken.getIssuedAtDateFromToken(accessToken).getTime();
            jwtVo.setAccessToken(accessToken);
            jwtVo.setAccessExpiration(accessExpiration);
            jwtVo.setCreateTime(createTime);
            return AjaxResult.success(jwtVo);
        }
        return AjaxResult.error("登陆失败");
    }

LoginVo

package com.luoyuanxiangvip.admin.core.vo;

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

import javax.validation.constraints.NotBlank;
import java.io.Serializable;

/**
 * <p>
 *
 * </p>
 *
 * @author luoyuanxiang <p>luoyuanxiangvip.com</p>
 * @since 2019/5/27 10:19
 */
@Data
public class LoginVo implements Serializable {

    private static final long serialVersionUID = 7158840114330656683L;
    @ApiModelProperty(value = "登陆名")
    @NotBlank(message = "账号不能为空")
    private String username;

    @ApiModelProperty(value = "密码")
    @NotBlank(message = "密码不能为空")
    private String password;
}

JwtVo

package com.luoyuanxiangvip.admin.core.vo;

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

import java.io.Serializable;

/**
 * <p>
 *
 * </p>
 *
 * @author luoyuanxiang <p>luoyuanxiangvip.com</p>
 * @since 2019/5/27 10:19
 */
@Data
public class JwtVo implements Serializable {

    private static final long serialVersionUID = -1299095840932717137L;
    @ApiModelProperty(value = "请求token")
    private String accessToken;

    @ApiModelProperty(value = "请求token的过期时间 单位:秒")
    private long accessExpiration;

    @ApiModelProperty(value = "创建时间 单位:秒")
    private long createTime;
}

到此springboot前后端分离整合完毕,有不合理的地方望大神提出,谢谢


问渠哪得清如许,为有源头活水来。——朱熹