前些日子我曾經使用shiro來實現用戶的登錄,將賬號密碼托管給shiro,客戶端與服務端的連接通過cookie和session,
但是目前使用最多的登錄都是無狀態的,使用jwt或者oauth來實現登錄,所以也特地記錄一下。
1.第一步先添加jwt的依賴
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.7.0</version>
</dependency>
2.修改shiro的配置,大體上沒有什么大的變化,主要就是關閉session和配置jwt到shiro中
@Bean
public MyShiroRealm myShiroRealm(HashedCredentialsMatcher matcher){
MyShiroRealm myShiroRealm= new MyShiroRealm();
myShiroRealm.setCredentialsMatcher(matcher);
return myShiroRealm;
}
@Bean
public DefaultWebSecurityManager securityManager(HashedCredentialsMatcher matcher){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm(matcher));
/*
* 關閉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);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
//如果沒有此name,將會找不到shiroFilter的Bean
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilter(org.apache.shiro.mgt.SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//shiroFilterFactoryBean.setLoginUrl("/login"); //表示指定登錄頁面 (前后分離不適用)
//shiroFilterFactoryBean.setSuccessUrl("/user/list"); // 登錄成功后要跳轉的鏈接 (前后分離不適用)
Map<String,String> filterChainDefinitionMap = new LinkedHashMap<>();//攔截器, 配置不會被攔截的鏈接 順序判斷
//filterChainDefinitionMap.put("/login","anon"); //所有匿名用戶均可訪問到Controller層的該方法下
filterChainDefinitionMap.put("/userLogin","anon");
filterChainDefinitionMap.put("/image/**","anon");
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/fonts/**","anon");
filterChainDefinitionMap.put("/js/**","anon");
filterChainDefinitionMap.put("/logout","logout");
filterChainDefinitionMap.put("/**", "authc"); //authc:所有url都必須認證通過才可以訪問; anon:所有url都都可以匿名訪問
//filterChainDefinitionMap.put("/**", "user"); //user表示配置記住我或認證通過可以訪問的地址
// 添加自己的過濾器並且取名為jwt
LinkedHashMap<String, Filter> filterMap = new LinkedHashMap<>();
filterMap.put("jwt", jwtFilter());
shiroFilterFactoryBean.setFilters(filterMap);
// 過濾鏈定義,從上向下順序執行,一般將放在最為下邊
filterChainDefinitionMap.put("/**", "jwt");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean
public JwtFilter jwtFilter() {
return new JwtFilter();
}
/**
* SpringShiroFilter首先注冊到spring容器
* 然后被包裝成FilterRegistrationBean
* 最后通過FilterRegistrationBean注冊到servlet容器
* @return
*/
@Bean
public FilterRegistrationBean delegatingFilterProxy(){
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
DelegatingFilterProxy proxy = new DelegatingFilterProxy();
proxy.setTargetFilterLifecycle(true);
proxy.setTargetBeanName("shiroFilter");
filterRegistrationBean.setFilter(proxy);
return filterRegistrationBean;
}
@Bean(name = "hashedCredentialsMatcher")
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("MD5");
hashedCredentialsMatcher.setHashIterations(1024);// 設置加密次數
return hashedCredentialsMatcher;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(HashedCredentialsMatcher matcher) {//@Qualifier("hashedCredentialsMatcher")
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager(matcher));
return authorizationAttributeSourceAdvisor;
}
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
3.封裝token來替換Shiro原生Token,要實現AuthenticationToken接口
public class JwtToken implements AuthenticationToken {
private static final long serialVersionUID = -8451637096112402805L;
private String token;
public JwtToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
4.添加一個JwtUtil的工具類來操作token
public class JwtUtil {
/**
* 過期時間30分鍾
*/
public static final long EXPIRE_TIME = 30 * 60 * 1000;
/**
* 校驗token是否正確
* @param token 密鑰
* @param secret 用戶的密碼
* @return 是否正確
*/
public static boolean verify(String token, String username, String secret) {
try {
// 根據密碼生成JWT效驗器
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm).withClaim("username", username).build();
// 效驗TOKEN
DecodedJWT jwt = verifier.verify(token);
log.info(jwt+":-token is valid");
return true;
} catch (Exception e) {
log.info("The token is invalid{}",e.getMessage());
return false;
}
}
/**
* 獲得token中的信息無需secret解密也能獲得
* @return token中包含的用戶名
*/
public static String getUsername(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) {
log.error("error:{}", e.getMessage());
return null;
}
}
/**
* 生成簽名,5min(分鍾)后過期
* @param username 用戶名
* @param secret 用戶的密碼
* @return 加密的token
*/
public static String sign(String username, String secret) {
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(secret);
// 附帶username信息
return JWT.create()
.withClaim("username", username)
.withExpiresAt(date)
.sign(algorithm);
}
}
5.寫一個攔截器JwtFilter,繼承BasicHttpAuthenticationFilter類
@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter {
@Autowired
private RedisUtil redisUtil;
private AntPathMatcher antPathMatcher =new AntPathMatcher();
/**
* 執行登錄認證(判斷請求頭是否帶上token)
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
log.info("JwtFilter-->>>isAccessAllowed-Method:init()");
//如果請求頭不存在token,則可能是執行登陸操作或是游客狀態訪問,直接返回true
if (isLoginAttempt(request, response)) {
return true;
}
//如果存在,則進入executeLogin方法執行登入,檢查token 是否正確
try {
executeLogin(request, response);return true;
} catch (Exception e) {
throw new AuthenticationException("Token失效請重新登錄");
}
}
/**
* 判斷用戶是否是登入,檢測headers里是否包含token字段
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
log.info("JwtFilter-->>>isLoginAttempt-Method:init()");
HttpServletRequest req = (HttpServletRequest) request;
if(antPathMatcher.match("/userLogin",req.getRequestURI())){
return true;
}
String token = req.getHeader(CommonConstant.ACCESS_TOKEN);
if (token == null) {
return false;
}
Object o = redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token);
if(ObjectUtils.isEmpty(o)){
return false;
}
log.info("JwtFilter-->>>isLoginAttempt-Method:返回true");
return true;
}
/**
* 重寫AuthenticatingFilter的executeLogin方法丶執行登陸操作
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
log.info("JwtFilter-->>>executeLogin-Method:init()");
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader(CommonConstant.ACCESS_TOKEN);//Access-Token
JwtToken jwtToken = new JwtToken(token);
// 提交給realm進行登入,如果錯誤他會拋出異常並被捕獲, 反之則代表登入成功,返回true
getSubject(request, response).login(jwtToken);return true;
}
/**
* 對跨域提供支持
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
log.info("JwtFilter-->>>preHandle-Method:init()");
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"));
// 跨域時會首先發送一個option請求,這里我們給option請求直接返回正常狀態
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
}
6.修改自定義的Realm
public class MyShiroRealm extends AuthorizingRealm {
@Autowired
private RoleService roleService;
@Autowired
private UserService userService;
@Autowired
private PermissionService permissionService;
@Autowired
private RedisUtil redisUtil;
/**
* 必須重寫此方法,不然Shiro會報錯
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* 訪問控制。比如某個用戶是否具有某個操作的使用權限
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
User user = (User) principalCollection.getPrimaryPrincipal();if (user == null) {
log.error("授權失敗,用戶信息為空!!!");
return null;
}
try {
//獲取用戶角色集
Set<String> listRole= roleService.findRoleByUsername(user.getUserName());
simpleAuthorizationInfo.addRoles(listRole);
//通過角色獲取權限集
for (String role : listRole) {
Set<String> permission= permissionService.findPermissionByRole(role);
simpleAuthorizationInfo.addStringPermissions(permission);
}
return simpleAuthorizationInfo;
} catch (Exception e) {
log.error("授權失敗,請檢查系統內部錯誤!!!", e);
}
return simpleAuthorizationInfo;
}
/**
* 用戶身份識別(登錄")
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String token = (String) authenticationToken.getCredentials();// 校驗token有效性
String username = JwtUtil.getUsername(token);if (Strings.isNullOrEmpty(username)) {
throw new AuthenticationException("token非法無效!");
}// 查詢用戶信息
User sysUser = userService.selectUserOne(username);
if (sysUser == null) {
throw new AuthenticationException("用戶不存在!");
}// 判斷用戶狀態
if (sysUser.getValid()==0) {
throw new AuthenticationException("賬號已被禁用,請聯系管理員!");
}
return new SimpleAuthenticationInfo(sysUser,token,ByteSource.Util.bytes(sysUser.getSalt()),getName());
}
}
7.登錄接口修改
public class LoginController {
@Autowired
private UserMapper userMapper;
@Autowired
private RedisUtil redisUtil;
/**
* 登錄
* @return
*/
@PostMapping(value = "/userLogin")
@ResponseBody
public Result<JSONObject> toLogin(@RequestBody User loginUser) throws Exception {
Result<JSONObject> result = new Result<>();
String userName = loginUser.getUserName();
String passWord = loginUser.getPassWord();
User user=userMapper.selectUserOne(userName);
if (user == null) {
return result.error500("該用戶不存在");
}
if (user.getValid()==0) {
return result.error500("賬號已被禁用,請聯系管理員!");
}
//我的密碼是使用uuid作為鹽值加密的,所以這里登陸時候還需要做一次對比
SimpleHash simpleHash = new SimpleHash("MD5", passWord, user.getSalt(), 1024);
if(!simpleHash.toHex().equals(user.getPassWord())){
return result.error500("密碼不正確");
}
// 生成token
String token = JwtUtil.sign(userName, passWord);
redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, token,JwtUtil.EXPIRE_TIME / 1000);
JSONObject obj = new JSONObject();
obj.put("token", token);
obj.put("userInfo", user);
result.setResult(obj);
result.success("登錄成功");
return result;
}
}
添加的方法,這里的加密算法和加密次數以及鹽值都要一致,否則登錄時候密碼對比會失敗
@RequestMapping("/insertUser")
@ResponseBody
public int insertUser(User user){
//將uuid設置為密碼鹽值
String salt = UUID.randomUUID().toString().replaceAll("-","");
SimpleHash simpleHash = new SimpleHash("MD5", user.getPassWord(), salt, 1024);
user.setPassWord(simpleHash.toHex()).setValid(1).setSalt(salt).setCreateTime(new Date()).setDel(0);
return userMapper.insertSelective(user);
}
定義的常量
public class CommonConstant {
/**
* 刪除標志 1 未刪除 0
*/
public static final Integer DEL_FLAG_1 = 1;
public static final Integer DEL_FLAG_0 = 0;
public static final Integer SC_INTERNAL_SERVER_ERROR_500 = 500;
public static final Integer SC_OK_200 = 200;
/**
* 訪問權限認證未通過 510
*/
public static final Integer SC_JEECG_NO_AUTHZ = 510;
/**
* 登錄用戶令牌緩存KEY前綴
*/
public static final int TOKEN_EXPIRE_TIME = 3600; //3600秒即是一小時
public static final String PREFIX_USER_TOKEN = "PREFIX_USER_TOKEN_";
/**
* 0:一級菜單
*/
public static final Integer MENU_TYPE_0 = 0;
/**
* 1:子菜單
*/
public static final Integer MENU_TYPE_1 = 1;
/**
* 2:按鈕權限
*/
public static final Integer MENU_TYPE_2 = 2;
/**
* 是否用戶已被凍結 1(解凍)正常 2凍結
*/
public static final Integer USER_UNFREEZE = 1;
public static final Integer USER_FREEZE = 2;
/**
* token的key
*/
public static String ACCESS_TOKEN = "Access-Token";
/**
* 登錄用戶規則緩存
*/
public static final String LOGIN_USER_RULES_CACHE = "loginUser_cacheRules";
/**
* 登錄用戶擁有角色緩存KEY前綴
*/
public static String LOGIN_USER_CACHERULES_ROLE = "loginUser_cacheRules::Roles_";
/**
* 登錄用戶擁有權限緩存KEY前綴
*/
public static String LOGIN_USER_CACHERULES_PERMISSION = "loginUser_cacheRules::Permissions_";
}
目前只是一個shiro+jwt的簡單的登錄,第一次登錄的時候不需要攜帶token,登陸之后會返回一個token,然后可以拿着這個token去訪問其他接口,
能訪問證明成功,后來經過測試,JWTToken刷新生命周期這個方法有誤,第一次拿錯誤的token訪問會報錯token失效,但是第二次就可以登錄成功,
所以可以去掉那個方法,有大佬可以幫忙指出改正這個方法,如果你看到,希望能夠給我一些建議,感謝!!!