springboot項目中集成shiro+jwt完整實例代碼
簡介
現(xiàn)在主流的安全框架分別為Shiro和Spring Security。關于兩者之間的優(yōu)缺點不是本文的重點,有興趣的可以在網上搜搜,各種文章也都分析的很清楚。那么簡單來說,Shiro是一個強大易用的Java安全框架,提供了認證、授權、加密和會話管理等功能。(不一定要建立所謂的五張表,我們要做到控制自如的使用)
目的
通過集成shiro,jwt我們要實現(xiàn):用戶登錄的校驗;登錄成功后返回成功并攜帶具有身份信息的token以便后續(xù)調用接口的時候做認證;對項目的接口進行權限的限定等。
需要的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使用自定義注解屏蔽接口鑒權實現(xiàn)
- 配置shiro登錄驗證的密碼加密方式:Shiro 提供了用于加密密碼和驗證密碼服務的 CredentialsMatcher 接口,HashedCredentialsMatcher 正是 CredentialsMatcher 的一個實現(xiàn)類。
- LoginRealm:自定義的Realm,用于處理用戶登錄驗證的Realm,在shiro中驗證及授權等信息會在Realm中配置,詳細解釋請參考shiro簡介
- JwtRealm:自定義的Realm,用戶在登錄后訪問服務時做token的校驗,用戶權限的校驗等。
- 配置DataAuthModularRealmAuthenticator:是在項目中存在多個Realm時,根據項目的認證策略可以選擇匹配需要的Realm。
- SecurityManager:Shiro的核心組件,管理著認證、授權、會話管理等,在這里我把所有的自定義的Realm等資源加入到SecurityManager中
- Shiro的過濾器:定制項目的path過濾規(guī)則,并將我們自定義的Filter加入到Shiro中的shiroFilterFactoryBean中
2.創(chuàng)建自定義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及實現(xiàn)類
DataAuthModularRealmAuthenticator的doSingleRealmAuthentication(realm, authenticationToken)做檢驗的時候需要兩個參數,一個是Realm另一個是我們定義的儲存驗證信息的AuthenticationToken或者它的實現(xiàn)類。
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
此類用于處理不在登錄下必須攜帶發(fā)行的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());
}
}到此這篇關于springboot項目中集成shiro+jwt詳解+完整實例的文章就介紹到這了,更多相關springboot 集成shiro jwt內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
基于Java代碼實現(xiàn)數字在數組中出現(xiàn)次數超過一半
這篇文章主要介紹了基于Java代碼實現(xiàn)數字在數組中出現(xiàn)次數超過一半的相關資料,需要的朋友可以參考下2016-02-02

