本博文代碼:https://download.csdn.net/download/qq_39404258/12439869
基本概念從其他博文中看,此處不講。
該項目使用了springboot、mybaits-plus、jwt、shiro、redis。mybaits-plus基本沒用,只做了一次數據庫查詢,redis暫時不使用,登錄驗證成功後再追加redis操作。
先說一下大致思路:
登錄操作:訪問登錄接口-》通過數據庫判斷是否存在-》存在後,進行shiro登錄-》將登錄信息轉化爲jwt的token返回。
驗證操作:訪問驗證接口-》通過過濾器攔截此請求-》將傳入的token拿去shiro登錄(轉入自定義的realm處理)-》token解析正確驗證成功。
pom文件:有些可能沒用,按需索取
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!--日誌 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!-- mp 依賴 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>net.sf.json-lib</groupId>
<artifactId>json-lib</artifactId>
<version>2.4</version>
<classifier>jdk15</classifier>
</dependency>
<!--shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.4.1</version>
</dependency>
<!--jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<!--引入JWT依賴,由於是基於Java,所以需要的是java-jwt-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
</dependencies>
一些配置:
swagger
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket createRestApi() {
ParameterBuilder tokenPar = new ParameterBuilder();
List<Parameter> pars = new ArrayList<Parameter>();
tokenPar.name("Authorization").description("Authorization")
.modelRef(new ModelRef("string")).parameterType("header").required(false).build();
pars.add(tokenPar.build());
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("mptest.mybatistest"))
.paths(PathSelectors.any())
.build().globalOperationParameters(pars) ;
}
@SuppressWarnings("deprecation")
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("個人測試")
.description("個人測試用api")
.termsOfServiceUrl("termsOfServiceUrl")
.contact("測試")
.version("1.0")
.build();
}
}
重點配置shiro
@Configuration
public class ShiroConfig {
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必須設置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
//設置過濾器
Map<String, Filter> filtersMap = shiroFilterFactoryBean.getFilters();
filtersMap.put("jwt", new ShiroJWTFilter());
shiroFilterFactoryBean.setFilters(filtersMap);
// shiro內置過濾器
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
//swagger接口權限 開放
filterChainDefinitionMap.put("/swagger-ui.html", "anon");
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/v2/**", "anon");
filterChainDefinitionMap.put("/swagger-resources/**", "anon"); //swagger
filterChainDefinitionMap.put("/user/login", "anon");//後臺登錄
filterChainDefinitionMap.put("/**", "jwt"); //jwt token驗證
//默認登陸頁面
//shiroFilterFactoryBean.setLoginUrl("/user/login");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean("authenticator")
public Authenticator authenticator(){
ModularRealmAuthenticator authenticator = new UserModularRealmAuthenticator();
authenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
return authenticator;
}
@Bean("authorizer")
public Authorizer authorizer() {
Authorizer modularRealmAuthorizer = new ModularRealmAuthorizer();
Collection<Realm> realmCollection = new HashSet<>();
realmCollection.add(new UserRealm());
realmCollection.add(new TokenInvalidRealm());
((ModularRealmAuthorizer) modularRealmAuthorizer).setRealms(realmCollection);
return modularRealmAuthorizer;
}
@Bean(name="userRealm")
public UserRealm userRealm() {
return new UserRealm();
}
@Bean
public TokenInvalidRealm tokenInvalidRealm() {
return new TokenInvalidRealm();
}
@Bean(name="defaultWebSecurityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager() {
DefaultWebSecurityManager defaultWebSecurityManager=new DefaultWebSecurityManager();
// 設置realm.
defaultWebSecurityManager.setAuthorizer(authorizer());
defaultWebSecurityManager.setAuthenticator(authenticator());
defaultWebSecurityManager.setRealms(Arrays.asList(userRealm(),tokenInvalidRealm()));
return defaultWebSecurityManager;
}
}
來分析一下配置:
首頁去掉swagger的過濾(前四個),去掉登錄接口的過濾,其他所有接口都放入jwt過濾器,也就是自定義的ShiroJWTFilter(在訪問驗證時介紹)
// shiro內置過濾器
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
//swagger接口權限 開放
filterChainDefinitionMap.put("/swagger-ui.html", "anon");
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/v2/**", "anon");
filterChainDefinitionMap.put("/swagger-resources/**", "anon"); //swagger
filterChainDefinitionMap.put("/user/login", "anon");//後臺登錄
filterChainDefinitionMap.put("/**", "jwt"); //jwt token驗證
又因爲配置的是多realm(一個用來登錄邏輯、一個用來驗證邏輯),要配置一個 ModularRealmAuthenticator
去處理請求時的realm,我們來自己寫一個子類來處理。
/**
用於過濾該走哪些realm
*/
public class UserModularRealmAuthenticator extends ModularRealmAuthenticator {
private static final Logger logger = LoggerFactory.getLogger(UserModularRealmAuthenticator.class);
@Override
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken)
throws AuthenticationException {
// logger.info("UserModularRealmAuthenticator:method doAuthenticate() execute ");
// 判斷getRealms()是否返回爲空
assertRealmsConfigured();
// 所有Realm
Collection<Realm> realms = getRealms();
// 過濾, 根據 是否支持 判斷
List<Realm> lastReam = realms.stream().filter(current -> current.supports(authenticationToken)).collect(Collectors.toList());
if (CollectionUtils.isEmpty(lastReam)) {
Assert.notEmpty(lastReam, "realms is empty");
}
if (lastReam.size() == 1) {
return doSingleRealmAuthentication(lastReam.iterator().next(), authenticationToken);
} else {
return doMultiRealmAuthentication(lastReam, authenticationToken);
}
}
}
這段代碼核心就這一個:
拿到所有realm後,將符合條件的放入集合,按順序處理realm邏輯,這樣就沒必要每次請求都走一遍所有的realm。
Collection<Realm> realms = getRealms();
// 過濾, 根據 是否支持 判斷
List<Realm> lastReam = realms.stream().filter(current -> current.supports(authenticationToken)).collect(Collectors.toList());
這裏有一個坑,如果setAuthenticator在realm後面會出現這樣的錯
Configuration error: No realms have been configured! One or more realms must be present to execute an authentication attempt.
解決辦法:將setRealms放在setAuthorizer後面,先配置authorizer,再配置realm。
原因請參考博文:https://blog.csdn.net/u011833033/article/details/104018407
defaultWebSecurityManager.setAuthenticator(authenticator());
defaultWebSecurityManager.setRealms(Arrays.asList(userRealm(),tokenInvalidRealm()));
然後我們看一下兩個realm:
UserRealm:在doGetAuthenticationInfo裏進行了一次登錄操作
//AuthenticatingRealm只用做身份驗證 |AuthorizingRealm 用作身份驗證和權限驗證
public class UserRealm extends AuthenticatingRealm {
@Autowired
private UserDao userDao;
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UserToken;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("執行登錄認證方法!");
UsernamePasswordToken usertoken=(UsernamePasswordToken) token;
User user = userDao.selectOne(new QueryWrapper<User>().eq("username",usertoken.getUsername()));
if (user==null) {
System.out.println("驗證錯誤,拋出異常!");
return null;
}
System.out.println("執行doGetAuthenticationInfo認證方法完畢!");
//第一個參數userInfo對象對用的用戶名,第二個參數,傳的是獲取的password
return new SimpleAuthenticationInfo(usertoken.getUsername(),usertoken.getPassword(),"");
}
}
TokenInvalidRealm :在doGetAuthenticationInfo進行了一次登錄操作
public class TokenInvalidRealm extends AuthorizingRealm {
@Autowired
private UserDao userDao;
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JWTToken;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("執行登錄認證方法!");
JWTToken usertoken=(JWTToken) authenticationToken;
String token = usertoken.getToken();
DecodedJWT jwt = JWT.decode(token);
String username = jwt.getClaim("username").asString();
User user = userDao.selectOne(new QueryWrapper<User>().eq("username",username));
if (user==null) {
System.out.println("驗證錯誤,拋出異常!");
return null;
}
System.out.println("執行doGetAuthenticationInfo認證方法完畢!");
return new SimpleAuthenticationInfo(user,token,TokenInvalidRealm.class.getName());
}
}
然後這兩個裏面都有一個supports方法,用於判斷傳入的token類型,來判斷請求接口時用哪些realm進行邏輯處理。
重寫的兩個token類型:
/*
用作 JWTtoken驗證環境
*/
public class JWTToken implements AuthenticationToken {
private String token;
public JWTToken(String token){
this.token=token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
public String getToken(){
return token;
}
}
/*
shiro token驗證環境
*/
public class UserToken extends UsernamePasswordToken {
public UserToken(String username, String password) {
super(username, password);
}
}
配置說完了,然後來寫controller,因爲springboot、mybatis不是重點,所以不作介紹,省略dao、service層。
shiro框架中 執行這行代碼時subject.login(token);會進入realm的doGetAuthenticationInfo方法中。
@RestController
@RequestMapping("user")
public class LoginController {
//過期時間
private static long time=1000*60;
@Autowired
private UserDao userDao;
@PostMapping("/login")
public String login(String password,String username){
User user = userDao.selectOne(new QueryWrapper<User>().eq("username",username).eq("password",password));
if (user==null){
return "賬號或密碼錯誤";
}
UserToken token = new UserToken(username,password);
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
//指定簽名算法,header部分
Algorithm algorithm=Algorithm.HMAC256(password.getBytes(StandardCharsets.UTF_8));
Date expire=new Date(System.currentTimeMillis()+time);
//jwt token簽證
String authorization = JWT.create().withClaim("username",username).withExpiresAt(expire).sign(algorithm);
return authorization;
}catch (Exception e){
return "登錄驗證失敗";
}
}
@GetMapping("/shiroJWT")
public String shiroJWT(){
return "shiroJWT驗證成功";
}
}
我們訪問登錄接口,結果賬號密碼正確時返回jwt token
然後接着我們訪問驗證接口
因爲我們自定義的過濾器ShiroJWTFilter,這個請求符合過濾器規則,則進入該過濾器。
首頁會進入isAccessAllowed方法,然後執行executeLogin方法,將獲得的token進行login操作,因爲這個token是JWTToken 類型的,接着跳轉到TokenInvalidRealm的doGetAuthenticationInfo方法進行邏輯操作。如果驗證失敗則拋出異常結束,正確則進入controller進行邏輯執行。
public class ShiroJWTFilter extends AuthenticatingFilter {
public ShiroJWTFilter(){
this.setLoginUrl("/user/login");
}
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String authorization = httpServletRequest.getHeader("Authorization");
JWTToken token=new JWTToken(authorization);
//因爲login()方法裏得參數是AuthenticationToken,需將jwt簽證轉換爲AuthenticationToken
// 提交給realm進行登入,如果錯誤他會拋出異常並被捕獲
getSubject(request, response).login(token);
// 如果沒有拋出異常則代表登入成功,返回true
return true;
}
@Override
protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
return null;
}
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if(this.isLoginRequest(request, response)){
return true;
}else {
boolean allowed = false;
try {
allowed = executeLogin(request, response);
} catch (Exception e) {
System.out.println("失敗了");
}
return allowed || super.isPermissive(mappedValue);
}
}
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
String json="{\"code\":401,\"message\":\"token validation fails\"}";
httpServletResponse.setHeader("Content-type", "application/json;charset=UTF-8");
httpServletResponse.getWriter().write(json);
return false;
}
}
運行結果:
這樣,整個shiro和jwt整合就完成了。
本質上就是拿到token以後,之後的每個請求都要攜帶token,而每次請求其實都重新做了一次登錄操作。
如果每次都查一下數據庫則效率會降低,那麼我們第一次登錄時就將用戶信息存入緩存,之後每次拿只需要去緩存中去取。
增加了redis工具類:
public class JedisUtil {
private static JedisPool jp;
static {
JedisPoolConfig jpc=new JedisPoolConfig();
jpc.setMaxIdle(10);//最大空閒
jpc.setMaxTotal(30);//最大連接
jp= new JedisPool(jpc,"127.0.0.1",6379);
}
public static Jedis getJedis(){
return jp.getResource();
}
}
修改controller
@PostMapping("/login")
public String login(String password,String username){
User user = userDao.selectOne(new QueryWrapper<User>().eq("username",username).eq("password",password));
if (user==null){
return "賬號或密碼錯誤";
}
Jedis jedis= JedisUtil.getJedis();
jedis.set(username,password);
UserToken token = new UserToken(username,password);
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
//指定簽名算法,header部分
Algorithm algorithm=Algorithm.HMAC256(password.getBytes(StandardCharsets.UTF_8));
Date expire=new Date(System.currentTimeMillis()+time);
//jwt token簽證
String authorization = JWT.create().withClaim("username",username).withExpiresAt(expire).sign(algorithm);
return authorization;
}catch (Exception e){
return "登錄驗證失敗";
}
}
修改驗證的realm
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("執行登錄認證方法!");
JWTToken usertoken=(JWTToken) authenticationToken;
String token = usertoken.getToken();
DecodedJWT jwt = JWT.decode(token);
String username = jwt.getClaim("username").asString();
Jedis jedis = JedisUtil.getJedis();
String pawd = jedis.get(username);
User user=null;
if (pawd==null){
user = userDao.selectOne(new QueryWrapper<User>().eq("username",username));
if (user==null) {
System.out.println("驗證錯誤,拋出異常!");
return null;
}
jedis.set(user.getUsername(),user.getPassword());
System.out.println("執行doGetAuthenticationInfo認證方法完畢!");
//都是SimpleAuthenticationInfo爲什麼兩次傳的不一樣
return new SimpleAuthenticationInfo(user,token,TokenInvalidRealm.class.getName());
}else {
System.out.println("執行doGetAuthenticationInfo認證方法完畢!");
//都是SimpleAuthenticationInfo爲什麼兩次傳的不一樣
return new SimpleAuthenticationInfo(username,token,TokenInvalidRealm.class.getName());
}
}