springboot集成shiro+jwt详解+完整实例

目录

简介

目的

需要的jar

集成过程

1.配置shiro

2.创建自定义Realm

2.1 LoginRealm用于处理用户登录

2.2  JwtRealm用于在登录之后,用户的token是否正确以及给当前用户授权等

2.3 OurModularRealmAuthenticator用于匹配的相应的Realm

2.4 DataAutoToken及实现类

2.5 JwtFilter处理在shiro配置的自定义的Filter

2.6 controller层登录和其他接口

2.7 service层

2.8 jwt工具类

2.9 其他的一些工具类

2.10 返回结果定义


简介

现在主流的安全框架分别为Shiro和Spring Security。关于两者之间的优缺点不是本文的重点,有兴趣的可以在网上搜搜,各种文章也都分析的很清楚。那么简单来说,Shiro是一个强大易用的Java安全框架,提供了认证、授权、加密和会话管理等功能。(不一定要建立所谓的五张表,我们要做到控制自如的使用

目的

通过集成shiro,jwt我们要实现:用户登录的校验;登录成功后返回成功并携带具有身份信息的token以便后续调用接口的时候做认证;对项目的接口进行权限的限定等。

需要的jar

本文使用的gradel作为jar包管理工具,maven也是使用相同的jar

//shiro的jar
implementation 'org.apache.shiro:shiro-spring:1.7.1'
//jwt的jar
implementation 'com.auth0:java-jwt:3.15.0'
implementation 'com.alibaba:fastjson:1.2.76'

compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

集成过程

1.配置shiro

@Configuration
public class ShiroConfig {
    /*
     * 解决spring aop和注解配置一起使用的bug。如果您在使用shiro注解配置的同时,引入了spring
     * aop的starter,会有一个奇怪的问题,导致shiro注解的请求,不能被映射 
     */  
    @Bean
    public static DefaultAdvisorAutoProxyCreator creator() {
        DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
        creator.setProxyTargetClass(true);
        return creator;
    }

    /**
     * Enable Shiro AOP annotation support. --<1>
     *
     * @param securityManager Security Manager
     * @return AuthorizationAttributeSourceAdvisor
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    /**
     * Use for login password matcher --<2>
     *
     * @return HashedCredentialsMatcher
     */
    @Bean("hashedCredentialsMatcher")
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        // set name of hash
        matcher.setHashAlgorithmName("SHA-256");
        // Storage format is hexadecimal
        matcher.setStoredCredentialsHexEncoded(true);
        return matcher;
    }

    /**
     * Realm for login --<3>
     *
     * @param matcher password matcher
     * @return PasswordRealm
     */
    @Bean
    public LoginRealm loginRealm(@Qualifier("hashedCredentialsMatcher") HashedCredentialsMatcher matcher) {
        LoginRealm loginRealm = new LoginRealm(LOGIN);
        loginRealm.setCredentialsMatcher(matcher);
        return loginRealm;
    }

    /**
     * JwtReal, use for token validation --<4>
     *
     * @return JwtRealm
     */
    @Bean
    public JwtRealm jwtRealm() {
        return new JwtRealm(JWT);
    }

    //  --<5>
    @Bean
    public OurModularRealmAuthenticator userModularRealmAuthenticator() {
        // rewrite ModularRealmAuthenticator
        DataAuthModularRealmAuthenticator modularRealmAuthenticator = new DataAuthModularRealmAuthenticator();
        modularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
        return modularRealmAuthenticator;
    }


    // --<6>
    @Bean(name = "securityManager")
    public SecurityManager securityManager(
            @Qualifier("userModularRealmAuthenticator") OurModularRealmAuthenticatormodular,
            @Qualifier("jwtRealm") JwtRealm jwtRealm,
            @Qualifier("loginRealm") LoginRealm loginRealm
    ) {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        // set realm
        manager.setAuthenticator(modular);
        // set to use own realm
        List realms = new ArrayList<>();
        realms.add(loginRealm);
        realms.add(jwtRealm);
        manager.setRealms(realms);
        // close Shiro's built-in session
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        manager.setSubjectDAO(subjectDAO);

        return manager;
    }

    // --<7>
    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map filter = new LinkedHashMap<>(1);
        filter.put("jwt", new JwtFilter());
        shiroFilterFactoryBean.setFilters(filter);

        Map filterMap = new HashMap<>();
        filterMap.put("/login/**", "anon");
        filterMap.put("/v1/**", "jwt");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
        return shiroFilterFactoryBean;
    }
}
  1. 开启shiro注解支持,具体原理请参考shiro中AuthorizationAttributeSourceAdvisor作用

  2. 配置shiro登录验证的密码加密方式:Shiro 提供了用于加密密码验证密码服务的 CredentialsMatcher 接口,HashedCredentialsMatcher 正是 CredentialsMatcher 的一个实现类。

  3. LoginRealm:自定义的Realm,用于处理用户登录验证的Realm,在shiro中验证及授权等信息会在Realm中配置,详细解释请参考shiro简介

  4. JwtRealm:自定义的Realm,用户在登录后访问服务时做token的校验,用户权限的校验等。

  5. 配置DataAuthModularRealmAuthenticator:是在项目中存在多个Realm时,根据项目的认证策略可以选择匹配需要的Realm。

  6. SecurityManager:Shiro的核心组件,管理着认证、授权、会话管理等,在这里我把所有的自定义的Realm等资源加入到SecurityManager中

  7. Shiro的过滤器:定制项目的path过滤规则,并将我们自定义的Filter加入到Shiro中的shiroFilterFactoryBean中

2.创建自定义Realm

2.1 LoginRealm用于处理用户登录

public class LoginRealm extends AuthorizingRealm {
    public LoginRealm(String name) {
        setName(name);
    }

    // 获取user相关信息的service类
    @Autowired
    private UserLoginService userLoginService;

    // supports方法必须重写,这是shiro处理流程中的一部分,他会通过此方法判断realm是否匹配的正确
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof LoginDataAutoToken;
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
        LoginDataAutoToken token = (LoginDataAutoToken) auth;
        serviceLog.info(token.getUsername() + "password auth start...");
        User user = userLoginService.selectUserByName(token.getUsername());
        if (user == null) throw new UnknownAccountException();
        Object credentials = user.getPassword();
        // save username and role to Attribute
        ServletUtils.userNameRoleTo.accept(user.getUserName(), (int) user.getUserType());
        return new SimpleAuthenticationInfo(user, credentials, super.getName());
    }
}

2.2  JwtRealm用于在登录之后,用户的token是否正确以及给当前用户授权等

public class JwtRealm extends AuthorizingRealm {
    public JwtRealm(String name) {
        setName(name);
    }

    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtDataAutoToken;
    }

    // 给当前用户授权,只有在访问的接口上配置了shiro的权限相关的注解的时候才会进入此方法
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        UserEnum.Type userEnum = EnumValue.dataValueOf(
                UserEnum.Type.class,
                ServletUtils.userNameRoleFrom.get().getUserRole()
        );
        Set roles = new HashSet<>();
        roles.add(userEnum.getDesc());
        // 授权角色如果有其他的权限则都已此类的方式授权
        authorizationInfo.setRoles(roles);
        return authorizationInfo;
    }

    // 验证此次request携带的token是否正确,如果正确解析当前token,并存入上下文中
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
        // verify token
        String token = (String) auth.getCredentials();
        TokenUtils.verify(token);
        TupleNameRole tupleNameRole = TokenUtils.tokenDecode(token);
        ServletUtils.userNameRoleTo.accept(tupleNameRole.getUsername(), tupleNameRole.getUserRole());
        return new SimpleAuthenticationInfo(token, token, ((JwtDataAutoToken) auth).getName());
    }
}

2.3 OurModularRealmAuthenticator用于匹配的相应的Realm

public class DataAuthModularRealmAuthenticator extends ModularRealmAuthenticator {
    @Override
    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
        assertRealmsConfigured();
        DataAutoToken dataAutoToken = (DataAutoToken) authenticationToken;

        Realm realm = getRealm(dataAutoToken);
        return doSingleRealmAuthentication(realm, authenticationToken);
    }

    private Realm getRealm(DataAutoToken dataAutoToken) {
        for (Realm realm : getRealms()) {
            // 根据定义的realm的name和dataAutoToken的name匹配相应的realm 
            if (realm.getName().contains(dataAutoToken.getName())) {
                return realm;
            }
        }
        return null;
    }
}

2.4 DataAutoToken及实现类

DataAuthModularRealmAuthenticator的doSingleRealmAuthentication(realm, authenticationToken)做检验的时候需要两个参数,一个是Realm另一个是我们定义的储存验证信息的AuthenticationToken或者它的实现类。

DataAutoToken:

public interface DataAutoToken {
    String getName();
}

 LoginDataAutoToken :

public class LoginDataAutoToken extends UsernamePasswordToken implements DataAuthToken {
    public LoginDataAuthToken(final String username, final String password) {
        super(username, password);
    }

    @Override
    public String getName() {
        return LOGIN;
    }
}

 JwtDataAutoToken:

public class JwtDataAutoToken implements AuthenticationToken, DataAuthToken {
    private final String token;

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

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

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

    @Override
    public String getName() {
        return JWT;
    }
}

2.5 JwtFilter处理在shiro配置的自定义的Filter

此类用于处理不在登录下必须携带发行的Token访问接口,如果Token存在,则使用shiro subject做token的和访问权限的校验。

public class JwtFilter extends BasicHttpAuthenticationFilter {
    private final BiConsumer writeResponse = (response, message) ->
            Utils.renderString.accept(
                    (HttpServletResponse) response,
                    JSON.toJSONString(ResponseResult.fail(message), SerializerFeature.WriteMapNullValue)
            );

    /**
     * @param request     ServletRequest
     * @param response    ServletResponse
     * @param mappedValue mappedValue
     * @return 是否成功
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        //input request to request log file
        requestLog.info(
                "path:{}, method:{}",
                httpServletRequest.getServletPath(),
                httpServletRequest.getMethod()
        );
        String token = httpServletRequest.getHeader(Constant.TOKEN);
        if (token != null) {
            return executeLogin(request, response);
        } else {
            writeResponse.accept(response, ErrorMessage.TOKEN_NOT_EXIST);
            return false;
        }
    }

    /**
     * execute login
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String token = httpServletRequest.getHeader(Constant.TOKEN);
        try {
            JwtDataAuthToken jwtToken = new JwtDataAuthToken(token);
            // validate user permission
            getSubject(request, response).login(jwtToken);
            return true;
        } catch (AuthenticationException e) {
            Throwable throwable = e.getCause();
            if (throwable instanceof TokenExpiredException) {
                writeResponse.accept(response, ErrorMessage.TOKEN_HAS_EXPIRED);
            } else {
                writeResponse.accept(response, ErrorMessage.TOKEN_INVALID);
            }
        }
        return false;
    }

    /**
     * support across domains
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        httpServletResponse.setHeader("Access-Control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));

        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }

2.6 controller层登录和其他接口

@RestController
public class AuthController {
    @Autowired
    private UserService userService;

    @PostMapping("/login")
    public ResponseResult login(@RequestBody UserReqDto userReqDto) {
        userService.login(userLoginReqDto.getUsername(), userReqDto.getPassword());
        return ResponseResult.success();
    }

    // shiro角色注解,admin才可以访问此接口
    @RequiresRoles("admin")
    @PostMapping("/v1/user")
    public ResponseResult addUser(@RequestBody UserAddReqDto userAddReqDto) {
        userService.add(userAddReqDto);
        return ResponseResult.success();
    }


    @PostMapping("/v1/token/verify")
    public ResponseResult verify() {
        return ResponseResult.success(false);
    }

 
    @PostMapping("/v1/token/refresh")
    public ResponseResult refresh() {
        return ResponseResult.success();
    }
}

2.7 service层

@Service
public class UserServiceImpl implements UserService {
    @Override
    public void login(String username, String password) {
        // Use shiro to verify the username and password
        Subject subject = SecurityUtils.getSubject();
        LoginDataAutoToken token = new LoginDataAutoToken(username, password);
        subject.login(token);
    }

    @Transactional
    @Override
    public void add(UserAddReqDto dto) {
        User user = getUserByName.apply(dto.getUsername());
        if (user != null) {
            throw new DataAuthException(ErrorMessage.USER_ALREADY_EXISTS);
        } else {
            User newUser = new User();
            // 设置user的信息
            post(newUser); // insert user to database
        }
}

2.8 jwt工具类

public final class TokenUtils {
    private TokenUtils() {
    }

    /**
     * @param username username
     * @param role     user role
     * @return The encrypted token
     */
    public static String createToken(String username, int role) {
        Date date = new Date(System.currentTimeMillis() + Constant.TOKEN_EXPIRE_TIME);
        Algorithm algorithm = Algorithm.HMAC256(username);
        return JWT.create()
                .withClaim(Constant.USER_NAME, username)
                .withClaim(Constant.USER_ROLE, role)
                .withExpiresAt(date)
                .sign(algorithm);
    }

    /**
     * @param username username
     * @param role     user role
     * @return The encrypted token
     */
    public static String refreshToken(String username, int role) {
        return createToken(username, role);
    }

    /**
     * refresh token and add to header
     */
    public static void refreshToken() {
        TupleNameRole tupleNameRole = ServletUtils.userNameRoleFrom.get();
        ServletUtils.addHeader.accept(
                Constant.TOKEN,
                createToken(tupleNameRole.getUsername(), tupleNameRole.getUserRole())
        );
    }

    /**
     * verify token
     *
     * @param token jwtToken
     */
    public static void verify(String token) {
        try {
            TupleNameRole tupleNameRole = tokenDecode(token);
            Algorithm algorithm = Algorithm.HMAC256(tupleNameRole.getUsername());
            JWTVerifier verifier = JWT.require(algorithm)
                    .withClaim(Constant.USER_NAME, tupleNameRole.getUsername())
                    .withClaim(Constant.USER_ROLE, tupleNameRole.getUserRole())
                    .build();
            verifier.verify(token);
        } catch (JWTVerificationException e) {
            serviceLog.error("token verify fail.", e);
            throw e;
        }
    }

    /**
     * @param token token
     * @return user name and role
     */
    public static TupleNameRole tokenDecode(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return new TupleNameRole(
                    jwt.getClaim(Constant.USER_NAME).asString(),
                    jwt.getClaim(Constant.USER_ROLE).asInt()
            );
        } catch (JWTDecodeException e) {
            serviceLog.error("Token decode happen exception.", e);
            throw e;
        }
    }
}

2.9 其他的一些工具类

ServletUtils:与spring context中有关的一些方法
public final class ServletUtils {
    private ServletUtils() {
    }

    private static final int SCOPE = RequestAttributes.SCOPE_REQUEST;

    private static final Supplier servletRequestAttributes = () ->
            (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

    private static final Supplier request = () -> servletRequestAttributes.get().getRequest();

    private static final Supplier response = () -> servletRequestAttributes.get().getResponse();

    private static final Consumer saveUsernameToAttribute = (name) ->
            servletRequestAttributes.get().setAttribute(Constant.USER_NAME, name, SCOPE);

    private static final Supplier usernameFromAttribute = () ->
            (String) servletRequestAttributes.get().getAttribute(Constant.USER_NAME, SCOPE);

    private static final Consumer saveUserRoleToAttribute = (role) ->
            servletRequestAttributes.get().setAttribute(Constant.USER_ROLE, role, SCOPE);

    private static final Supplier userRoleFromAttribute = () ->
            (Integer) servletRequestAttributes.get().getAttribute(Constant.USER_ROLE, SCOPE);

    /**
     * get token form current request
     */
    public static Supplier tokenFromRequest = () -> request.get().getHeader(Constant.TOKEN);

    /**
     * save current user name and role to attribute
     */
    public static BiConsumer userNameRoleTo = (name, role) -> {
        saveUsernameToAttribute.accept(name);
        saveUserRoleToAttribute.accept(role);
    };

    /**
     * get user name and role from attribute
     */
    public static Supplier userNameRoleFrom = () ->
            new TupleNameRole(usernameFromAttribute.get(), userRoleFromAttribute.get());

    /**
     * add message to response header
     */
    public static BiConsumer addHeader = (key, value) -> response.get().addHeader(key, value);
}

Utils:提供与shiro相同的密码加密方式、获取uuid、shiro的Filter层出错不能使用全局异常处理时的返回信息定制等。

public final class Utils {
    private Utils() {
    }

    /**
     * use sha256 encrypt
     */
    public static Function encryptPassword = (password) -> new Sha256Hash(password).toString();

    /**
     * get uuid
     */
    public static Supplier uuid = () -> UUID.randomUUID().toString().replace("-", "");

    /**
     * writer message to response
     */
    public static BiConsumer renderString = (response, body) -> {
        response.setStatus(HttpStatus.OK.value());
        response.setCharacterEncoding("utf-8");
        response.setContentType("application/json;charset=UTF-8");
        try (PrintWriter writer = response.getWriter()) {
            writer.print(body);
        } catch (IOException e) {
            serviceLog.error("response error.", e);
        }
    };
}

2.10 返回结果定义

@Data
public class ResponseResult implements Serializable {
    private static final long serialVersionUID = 1L;

    private final String code;

    @JSONField(ordinal = 1)
    private final String msg;

    @JSONField(ordinal = 2)
    private T data;

    private ResponseResult(String code, String msg) {
        this.code = code;
        this.msg = msg;
        log();
    }

    private static  ResponseResult create(String code, String msg) {
        return new ResponseResult<>(code, msg);
    }

    /**
     * No data returned successfully
     *
     * @return ResponseResult
     */
    public static  ResponseResult success() {
        return success(true);
    }

    /**
     * No data returned successfully
     *
     * @param refreshToken Whether to refresh token
     * @return ResponseResult
     */
    public static  ResponseResult success(boolean refreshToken) {
        if (refreshToken) TokenUtils.refreshToken();
        return create(ErrorMessage.SUCCESS.code(), ErrorMessage.SUCCESS.msg());
    }

    public static  ResponseResult success(T data) {
        return success(data, true);
    }

    /**
     * Data returned successfully
     *
     * @param data         data
     * @param           T
     * @param refreshToken Whether to refresh token
     * @return ResponseResult
     */
    public static  ResponseResult success(T data, boolean refreshToken) {
        ResponseResult responseResult = success(refreshToken);
        responseResult.setData(data);
        return responseResult;
    }

    /**
     * @param e DCException
     * @return ResponseResult
     */
    public static ResponseResult fail(DataAuthException e) {
        return create(e.getCode(), e.getMsg());
    }

    /**
     * @param errorMessage ErrorMessage
     * @return ResponseResult
     */
    public static ResponseResult fail(ErrorMessage errorMessage) {
        return create(errorMessage.code(), errorMessage.msg());
    }

    /**
     * @param errorMessage DCException
     * @return ResponseResult
     */
    public static ResponseResult fail(ErrorMessage errorMessage, Object[] detailMessage) {
        return create(errorMessage.code(), errorMessage.msg() + Arrays.toString(detailMessage));
    }

    // Output the information returned
    private void log() {
        requestLog.info("code:{}, msg:{}", this.getCode(), this.getMsg());
    }
}

 

你可能感兴趣的:(java,SpringBoot,shiro)