目录
2.2 JwtRealm用于在登录之后,用户的token是否正确以及给当前用户授权等
2.3 OurModularRealmAuthenticator用于匹配的相应的Realm
2.5 JwtFilter处理在shiro配置的自定义的Filter
简介
现在主流的安全框架分别为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注解支持,具体原理请参考shiro中AuthorizationAttributeSourceAdvisor作用
-
配置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(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<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 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<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 msg) {
return new ResponseResult<>(code, msg);
}
/**
* 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());
}
}