本博文代码: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文件:有些可能没用,按需索取
org.springframework.boot
spring-boot-starter-data-jpa
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-devtools
runtime
true
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
org.junit.vintage
junit-vintage-engine
mysql
mysql-connector-java
runtime
io.springfox
springfox-swagger2
2.9.2
io.springfox
springfox-swagger-ui
2.9.2
com.baomidou
mybatis-plus-boot-starter
3.2.0
net.sf.json-lib
json-lib
2.4
jdk15
org.apache.shiro
shiro-spring-boot-web-starter
1.4.1
redis.clients
jedis
2.9.0
io.jsonwebtoken
jjwt
0.9.1
com.auth0
java-jwt
3.4.0
一些配置:
swagger
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket createRestApi() {
ParameterBuilder tokenPar = new ParameterBuilder();
List pars = new ArrayList();
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 filtersMap = shiroFilterFactoryBean.getFilters();
filtersMap.put("jwt", new ShiroJWTFilter());
shiroFilterFactoryBean.setFilters(filtersMap);
// shiro内置过滤器
Map filterChainDefinitionMap = new LinkedHashMap();
//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 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 filterChainDefinitionMap = new LinkedHashMap();
//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 realms = getRealms();
// 过滤, 根据 是否支持 判断
List 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 realms = getRealms();
// 过滤, 根据 是否支持 判断
List 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().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().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().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().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().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());
}
}