同类博客:SpringSecurity整合springboot+jwt
目录
目录结构
依赖
配置文件
代码介绍
SecurityProperties
AuthController
ShiroConfiguration
JwtAuthFilter
DbShiroRealm
JwtShiroRealm
JwtToken
JwtTokenProvider
用于权限测试的接口类
操作
github地址
红框中是核心配置文件
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.3.0.RELEASE
com.siyang
shiro-springboot-demo
0.0.1-SNAPSHOT
shiro-springboot-demo
Demo project for Spring Boot
1.8
org.springframework.boot
spring-boot-starter-data-jpa
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-data-redis
org.apache.shiro
shiro-spring-boot-web-starter
1.4.1
mysql
mysql-connector-java
runtime
com.alibaba
druid-spring-boot-starter
1.1.10
org.projectlombok
lombok
true
com.alibaba
fastjson
1.2.54
cn.hutool
hutool-all
5.0.6
io.jsonwebtoken
jjwt-api
0.10.6
io.jsonwebtoken
jjwt-impl
0.10.6
io.jsonwebtoken
jjwt-jackson
0.10.6
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-starter-test
test
org.junit.vintage
junit-vintage-engine
junit
junit
test
junit
junit
test
org.springframework.boot
spring-boot-maven-plugin
spring:
datasource:
druid:
url: jdbc:mysql://localhost:3306/securitydemo?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
jpa:
show-sql: true
hibernate:
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
#jwt
jwt:
header: Authorization
# 令牌前缀
token-start-with: Bearer
# 必须使用最少88位的Base64对该令牌进行编码
base64-secret: ZmQ0ZGI5NjQ0MDQwY2I4MjMxY2Y3ZmI3MjdhN2ZmMjNhODViOTg1ZGE0NTBjMGM4NDA5NzYxMjdjOWMwYWRmZTBlZjlhNGY3ZTg4Y2U3YTE1ODVkZDU5Y2Y3OGYwZWE1NzUzNWQ2YjFjZDc0NGMxZWU2MmQ3MjY1NzJmNTE0MzI=
# 令牌过期时间 此处单位/毫秒 ,默认4小时,可在此网站生成 https://www.convertworld.com/zh-hans/time/milliseconds.html
token-validity-in-seconds: 14400000
# 在线用户key
online-key: online-token
# 验证码
code-key: code-key
jwt配置文件参数映射
/**
* @author siyang
* @create 2020-01-12 14:45
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "jwt")//从yml配置文件中获取配置的属性
public class SecurityProperties {
/** Request Headers : Authorization */
private String header;
/** 令牌前缀,最后留个空格 Bearer */
private String tokenStartWith;
/** 必须使用最少88位的Base64对该令牌进行编码 */
private String base64Secret;
/** 令牌过期时间 此处单位/毫秒 */
private Long tokenValidityInSeconds;
/** 在线用户 key,根据 key 查询 redis 中在线用户的数据 */
private String onlineKey;
/** 验证码 key */
private String codeKey;
public String getTokenStartWith() {
return tokenStartWith + " ";
}
}
登录认证接口
/**
* @author siyang
* @create 2020-05-29 11:26
*/
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private SecurityProperties properties;
@Autowired
private JwtTokenProvider jwtTokenProvider;
@PostMapping("/login")
public ResponseEntity
shiro配置类,配置数据源,拦截路径,权限注解开启
@Configuration
public class ShiroConfiguration {
/**
* 设置过滤器
* @param securityManager
* @return
*/
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager, JwtTokenProvider jwtTokenProvider){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//配置安全管理
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map filters = shiroFilterFactoryBean.getFilters();
filters.put("authcToken", new JwtAuthFilter(jwtTokenProvider));
// filters.put("anyRole", createRolesFilter());
shiroFilterFactoryBean.setFilters(filters);
shiroFilterFactoryBean.setFilterChainDefinitionMap(shiroFilterChainDefinition().getFilterChainMap());
return shiroFilterFactoryBean;
}
/**
* 配置拦截路径及相应过滤器
* @return
*/
@Bean
protected ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
chainDefinition.addPathDefinition("/*.html", "anon");
chainDefinition.addPathDefinition("/**/*.html", "anon");
chainDefinition.addPathDefinition("/**/*.css", "anon");
chainDefinition.addPathDefinition("/**/*.js", "anon");
chainDefinition.addPathDefinition("/webSocket/**", "anon");
chainDefinition.addPathDefinition("/swagger-ui.html", "anon");
chainDefinition.addPathDefinition("/swagger-resources/**", "anon");
chainDefinition.addPathDefinition("/webjars/**", "anon");
chainDefinition.addPathDefinition("/*/api-docs", "anon");
chainDefinition.addPathDefinition("/avatar/**", "anon");
chainDefinition.addPathDefinition("/file/**", "anon");
chainDefinition.addPathDefinition("/druid/**", "anon");
chainDefinition.addPathDefinition("/avatar/**", "anon");
//控制器路径
chainDefinition.addPathDefinition("/", "anon");
chainDefinition.addPathDefinition("/auth/login", "anon");
chainDefinition.addPathDefinition("/auth/code", "anon");
chainDefinition.addPathDefinition("/auth/logout", "authcToken[permissive]");
chainDefinition.addPathDefinition("/**", "authcToken"); //对所有进行验证token
return chainDefinition;
}
/**
* 禁用session, 不保存用户登录状态。保证每次请求都重新认证。
* 需要注意的是,如果用户代码里调用Subject.getSession()还是可以用session,如果要完全禁用,要配合下面的noSessionCreation的Filter来实现
*/
@Bean
protected SessionStorageEvaluator sessionStorageEvaluator(){
DefaultWebSessionStorageEvaluator sessionStorageEvaluator = new DefaultWebSessionStorageEvaluator();
sessionStorageEvaluator.setSessionStorageEnabled(false);
return sessionStorageEvaluator;
}
/**
* 注册shiro的Filter,拦截请求
*/
@Bean
public FilterRegistrationBean filterRegistrationBean(SecurityManager securityManager, JwtTokenProvider jwtTokenProvider) throws Exception{
FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
filterRegistration.setFilter((Filter)shiroFilter(securityManager, jwtTokenProvider).getObject());
filterRegistration.addInitParameter("targetFilterLifecycle", "true");
filterRegistration.setAsyncSupported(true);
filterRegistration.setEnabled(true);
filterRegistration.setDispatcherTypes(DispatcherType.REQUEST);
return filterRegistration;
}
/**
* 初始化Authenticator 身份认证
*/
@Bean
public Authenticator authenticator() {
ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator();
//设置两个Realm,一个用于用户登录验证和访问权限获取;一个用于jwt token的认证
authenticator.setRealms(Arrays.asList(dbShiroRealm(),jwtShiroRealm()));
//设置多个realm认证策略,一个成功即跳过其它的
authenticator.setAuthenticationStrategy(new FirstSuccessfulStrategy());
return authenticator;
}
/**
* 初始化authorizer 认证器 权限认证
* @return
*/
@Bean
public Authorizer authorizer() {
ModularRealmAuthorizer authorizer = new ModularRealmAuthorizer();//这里的
authorizer.setRealms(Arrays.asList(jwtShiroRealm()));
return authorizer;
}
/**
* DbRealm,默认的密码校验算法为BCrypt
* @return
*/
@Bean("dbRealm")
public Realm dbShiroRealm() {
DbShiroRealm myShiroRealm = new DbShiroRealm();
//将Realm的默认密码校验设置为BCrypt算法
myShiroRealm.setCredentialsMatcher(new CredentialsMatcher() {
@Override
public boolean doCredentialsMatch(AuthenticationToken authenticationToken, AuthenticationInfo authenticationInfo) {
String password = new String(((UsernamePasswordToken) authenticationToken).getPassword());
String hashed = (String) authenticationInfo.getCredentials();
return BCrypt.checkpw(password,hashed);
}
});
return myShiroRealm;
}
/**
* jwtToken->Realm
* 校验前后token是否相同,其实可以直接返回true。
* 因为前面过滤器已经验证过token的完整性和正确性
* @return
*/
@Bean("jwtRealm")
public Realm jwtShiroRealm() {
JwtShiroRealm myShiroRealm = new JwtShiroRealm();
myShiroRealm.setCredentialsMatcher(new CredentialsMatcher() {
@Override
public boolean doCredentialsMatch(AuthenticationToken authenticationToken, AuthenticationInfo authenticationInfo) {
JwtToken jwtToken1 = (JwtToken) authenticationToken;
JwtToken jwtToken2 = (JwtToken)authenticationInfo.getCredentials();
String token1 = jwtToken1.getToken();
String token2 = jwtToken2.getToken();
return token1.equals(token2);
}
});
return myShiroRealm;
}
/**
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能
* @return
*/
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
creator.setProxyTargetClass(true);
return creator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
shiro过滤器
/**
* @author siyang
* @create 2020-01-12 20:40
* jwt过滤器
* 不能写@bean之类的直接,不能交给spring管理
*/
@Slf4j
public class JwtAuthFilter extends BasicHttpAuthenticationFilter {
private JwtTokenProvider jwtTokenProvider;
public JwtAuthFilter(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
/**
*父类会在请求进入拦截器后调用该方法,返回true则继续,返回false则会调用onAccessDenied()。这里在不通过时,还调用了isPermissive()方法,我们后面解释。
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
boolean allowed = false;
try {
//内部的方法会执行createToken
allowed = executeLogin(request, response);
} catch(IllegalStateException e){ //not found any token
log.error("Not found any token");
}catch (Exception e) {
log.error("Error occurs when login", e);
}
return allowed || super.isPermissive(mappedValue);
}
/**
* 这里重写了父类的方法,使用我们自己定义的Token类,提交给shiro。这个方法返回null的话会直接抛出异常,进入isAccessAllowed()的异常处理逻辑。
* @param request
* @param response
* @return
*/
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
//从request头中取出token
String token = jwtTokenProvider.getToken((HttpServletRequest) request);
//如果token不为空 并且token验证合格
if(token!=null){
// 返回的JWTtoken 会被JwtShiroRealm 进行解析
return new JwtToken(token);
}
return null;
}
/**
* 如果这个Filter在之前isAccessAllowed()方法中返回false,则会进入这个方法。我们这里直接返回错误的response
* @param servletRequest
* @param servletResponse
* @return
* @throws Exception
*/
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletResponse res = (HttpServletResponse)servletResponse;
res.setHeader("Access-Control-Allow-Origin", "*");
res.setStatus(HttpServletResponse.SC_OK);
res.setCharacterEncoding("UTF-8");
PrintWriter writer = res.getWriter();
Map map= new HashMap<>();
map.put("status", 401);
LocalDateTime now = LocalDateTime.now();
now.format(DateTimeFormatter.ISO_DATE_TIME);
map.put("timestamp", now.toString());
map.put("message", "身份认证失败");
writer.write(JSON.toJSONString(map));
writer.close();
return false;
}
}
登录验证数据源
/**
* @author siyang
* @create 2020-01-12 20:15
* 只需要登录验证,所以只需要继承AuthenticatingRealm
*/
public class DbShiroRealm extends AuthenticatingRealm {
@Autowired
private UserService userService;
/**
* 限定这个Realm只支持UsernamePasswordToken
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken;
}
/**
* 验证
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken usernamePasswordToken =(UsernamePasswordToken) authenticationToken;
String username = usernamePasswordToken.getUsername();
User user = userService.getUserByUsername(username);
if(user == null) {
// 账号不存在
throw new AuthenticationException();
}
return new SimpleAuthenticationInfo(user,user.getPassword(),"dbRealm");
}
}
jwt认证、授权数据源
/**
* @author siyang
* @create 2020-01-14 18:13
* 需要jwt验证和授权,所以需要继承AuthorizingRealm
*/
public class JwtShiroRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Autowired
private JwtTokenProvider jwtTokenProvider;
/**
* 限定这个Realm只支持我们自定义的JWT Token
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* 授权
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
JwtToken primaryPrincipal = (JwtToken)principalCollection.getPrimaryPrincipal();
String token = primaryPrincipal.getToken();
String username = jwtTokenProvider.parseToken(token);
User user = userService.getUserByUsername(username);
Set roles = user.getRoles();
Set roleSet = new HashSet<>();
Set permissionSet = new HashSet<>();
for (Role role : roles) {
roleSet.add(role.getName());
// 将角色下所有权限都存入permissions
Set p = role.getPermissions();
for (Permission permission : p) {
permissionSet.add(permission.getPermissionValue());
}
}
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.setRoles(roleSet);
simpleAuthorizationInfo.setStringPermissions(permissionSet);
return simpleAuthorizationInfo;
}
/**
* token登录,每次请求都要判断路径
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
JwtToken token = (JwtToken) authenticationToken;
// 如果解析jwt有问题会跳出
String username = jwtTokenProvider.parseToken(token.getToken());
User user = userService.getUserByUsername(username);
if(user == null) {
// 账号不存在
throw new AuthenticationException();
}
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(token,token,"jwtRealm");
return simpleAuthenticationInfo;
}
}
为UsernamePasswordToken的兄弟类,用于区分使用哪个数据源,这个是使用jwtToken,在数据源中support中指定
/**
* @author siyang
* @create 2020-01-14 17:48
* 封装的JwtToken对象
*/
public class JwtToken implements HostAuthenticationToken {
private String token;
private String host;
public JwtToken(String token) {
this(token, null);
}
public JwtToken(String token, String host) {
this.token = token;
this.host = host;
}
public String getToken(){
return this.token;
}
public String getHost() {
return host;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
@Override
public String toString(){
return token + ':' + host;
}
}
jwt提供工具类
/**
* @author siyang
* @create 2020-01-12 20:50
* JWT-TOKEN 提供类
*/
@Slf4j
@Component
public class JwtTokenProvider implements InitializingBean {
@Autowired
private SecurityProperties properties;
private static final String AUTHORITIES_KEY ="auth";
private Key key;
/**
* 实例化对前会调用此方法,需要Spring的环境
* @throws Exception
*/
@Override
public void afterPropertiesSet() throws Exception {
byte[] decode = Decoders.BASE64.decode(properties.getBase64Secret());
this.key= Keys.hmacShaKeyFor(decode);
}
/**
* 创建token
* @return
*/
public String createToken(String username){
long now = new Date().getTime();
Date date = new Date(now + properties.getTokenValidityInSeconds());
return Jwts.builder().setSubject(username).setExpiration(date).signWith(key, SignatureAlgorithm.HS512).compact();
}
/**
* 验证token
* @return
*/
public boolean vaildateToken (String token){
try {
Jwts.parser().setSigningKey(key).parseClaimsJws(token);
return true;
} catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
log.info("Invalid JWT signature.");
e.printStackTrace();
} catch (ExpiredJwtException e) {
log.info("Expired JWT token.");
e.printStackTrace();
} catch (UnsupportedJwtException e) {
log.info("Unsupported JWT token.");
e.printStackTrace();
} catch (IllegalArgumentException e) {
log.info("JWT token compact of handler are invalid.");
e.printStackTrace();
}
return false;
}
/**
* 根据token 解析出username
* @param token
* @return
*/
public String parseToken(String token){
Claims body = Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody();
String username = body.getSubject();
return username;
}
/**
* 从request中获取token
* @param request
* @return
*/
public String getToken(HttpServletRequest request){
String header = request.getHeader(properties.getHeader());
if(header != null && header.startsWith(properties.getTokenStartWith())){
header=header.substring(properties.getTokenStartWith().length());
}
return header;
}
}
/**
* @author siyang
* @create 2020-05-29 12:00
*/
@RestController
@RequestMapping("/")
public class UserController {
@Autowired
UserService userService;
@RequiresPermissions("read")
@GetMapping("getAll")
public ResponseEntity getAllUsers(){
List all = userService.findAll();
Map map = new HashMap<>();
map.put("list",all);
return ResponseEntity.ok(map);
}
}
使用resource/下的sql文件创建数据,然后使用postman测试接口,使用/auth/login登录 (sql文件存在2个用户siyang/lisi,密码123456)
然后登录成功会返回token信息,将它放入header中,header名为Authorization 。然后用get方式访问/getAll 接口获得用户信息,通过切换2个用户产生的token 值,可以看到权限的管理功能。
https://github.com/ebb94f53au/shiro-springboot-demo