本文主要是介绍springboot项目中集成shiro+jwt完整实例代码,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
《springboot项目中集成shiro+jwt完整实例代码》本文详细介绍如何在项目中集成Shiro和JWT,实现用户登录校验、token携带及接口权限管理,涉及自定义Realm、ModularRe...
简介
现在主流的安全框架分别为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<Realm> 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<String, Filter> filter = new LinkedHashMap<>(1); filter.put("jwt", new JwtFilter()); shiroFilterFactoryBean.setFilters(filter); Map<String, String> filterMap = new HashMap<>(); filterMap.put("/login/**", "anon"); filterMap.put("/v1/**", "jwt"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap); return shiroFilterFactoryBean; } }
- 开启shiro注解支持,具体原理请参考springboot中shiro使用自定义注解屏蔽接口鉴权实现
- 配置shiro登录验证的密码加密方式:Shiro 提供了用于加密密码和验证密码服务的 CredentialsMatcher 接口,HashedCredentialsMatcher 正是 CredentialsMatcher 的一个实现类。
- LoginRealm:自定义的Realm,用于处理用户登录验证的Realm,在shiro中验证及授权等信息会在Realm中配置,详细解释请参考shiro简介
- JwtRealm:自定义的Realm,用户在登录后访问服务时做token的校验,用户权限的校验等。
- 配置DataAuthModularRealmAuthenticator:是在项目中存在多个Realm时,根据项目的认证策略可以选择匹配需要的Realm。
- SecurityManager:Shiro的核心组件,管理着认证、授权、会话管理等,在这里我把所有的自定义的Realm等资源加入到SecurityManager中
- 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(usChina编程er.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<String> 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<ServletResponse, ErrorMessage> writeResponse = (response, message) -> Utils.renderString.accept( (HttpServletResponse) response, JSON.toJSONString(ResponseResult.fail(message), SerializerFeature.WriteMapNullValue) ); /** * @param request ServletRequeswww.chinasem.cnt * @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) { HttpSeeOvQmifmfDrvletRequest 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<String> login(@RequestBody UserReqDto userReqDto) { userService.login(userLoginReqDto.getUsername(), userReqDto.getPassword()); return ResponseResult.success(); } // shiro角色注解,admin才可以访问此接口 @RequiresRoles("admin") @PostMapping("/v1/user") public ResponseResult<String> addUser(@RequestBody UserAddReqDto userAddReqDto) { userService.add(userAddReqDto); return ResponseResult.success(); } @PostMapping("/v1/token/verify") public ResponseResult<String> verify() { return ResponseResult.success(false); } @PostMapping("/v1/token/refresh") public ResponseResult<String> 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 = () -> (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); private static final Supplier<HttpServletRequest> request = () -> servletRequestAttributes.get().getRequest(); private static final Supplier<HttpServletResponse> response = () -> servletRequestAttributes.get().getResponse(); private static final Consumer<String> saveUsernameToAttribute = (name) -> servletRequestAttributes.get().setAttribute(Constant.USER_NAME, name, SCOPE); private static final Supplier<String> usernameFromAttribute = () -> (String) servletRequestAttributes.get().getAttribute(Constant.USER_NAME, SCOPE); private static final Consumer<Integer> saveUserRoleToAttribute = (role) -> servletRequestAttributes.get().setAttribute(Constant.USER_ROLE, role, SCOPE); private static final Supplier<Integer> userRoleFromAttribute = () -> (Integer) servletRequestAttributes.get().getAttribute(Constant.USER_ROLE, SCOPE); /** * get token form current request */ public static Supplier<String> tokenFromRequest = () -> request.get().getHeader(Constant.TOKEN); /** * save current user name and role to attribute */ public static BiConsumer<String, Integer> userNameRoleTo = (name, role) -> { saveUsernameToAttribute.accept(name); saveUserRoleToAttribute.accept(role); }; /** * get user name and role from attribute */ public static Supplier<TupleNameRole> userNameRoleFrom = () -> new TupleNameRole(usernameFromAttribute.get(), userRoleFromAttribute.get()); /** * add message to response header */ public static BiConsumer<String, String> 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<String, String> encryptPassword = (password) -> new Sha256Hash(password).toString(); /** * get uuid */ public static Supplier<String> uuid = () -> UUID.randomUUID().toString().replace("-", ""); /** * writer message to response */ public static BiConsumer<HttpServletResponse, String> 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<T> 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 <T> ResponseResult<T> create(String code, String mshttp://www.chinasem.cng) { return new ResponseResult<>(code, msg); } /** android * No data returned successfully * * @return ResponseResult<String> */ public static <T> ResponseResult<T> success() { return success(true); } /** * No data returned successfully * * @param refreshToken Whether to refresh token * @return ResponseResult<String> */ public static <T> ResponseResult<T> success(boolean refreshToken) { if (refreshToken) TokenUtils.refreshToken(); return create(ErrorMessage.SUCCESS.code(), ErrorMessage.SUCCESS.msg()); } public static <T> ResponseResult<T> success(T data) { return success(data, true); } /** * Data returned successfully * * @param data data * @param <T> T * @param refreshToken Whether to refresh token * @return ResponseResult<T> */ public static <T> ResponseResult<T> success(T data, boolean refreshToken) { ResponseResult<T> responseResult = success(refreshToken); responseResult.setData(data); return responseResult; } /** * @param e DCException * @return ResponseResult<String> */ public static ResponseResult<String> fail(DataAuthException e) { return create(e.getCode(), e.getMsg()); } /** * @param errorMessage ErrorMessage * @return ResponseResult<String> */ public static ResponseResult<String> fail(ErrorMessage errorMessage) { return create(errorMessage.code(), errorMessage.msg()); } /** * @param errorMessage DCException * @return ResponseResult<String> */ public static ResponseResult<String> 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()); } }
到此这篇关于springboot项目中集成shiro+jwt详解+完整实例的文章就介绍到这了,更多相关springboot 集成shiro jwt内容请搜索编程China编程(www.chinasem.cn)以前的文章或继续浏览下面的相关文章希望大家以后多多支持China编程(www.chinasem.cn)!
这篇关于springboot项目中集成shiro+jwt完整实例代码的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!