同类博客:SpringSecurity整合springboot+jwt
目录
目录结构
红框中是核心配置文件
依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.siyang</groupId>
<artifactId>shiro-springboot-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>shiro-springboot-demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<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>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--Spring boot end-->
<!-- shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.4.1</version>
</dependency>
<!--Mysql依赖包-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- druid数据源驱动 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!--lombok插件-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.54</version>
</dependency>
<!--工具包 加密相关-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.0.6</version>
</dependency>
<!--jwt-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.10.6</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.10.6</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.10.6</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</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>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
配置文件
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
代码介绍
SecurityProperties
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 + " ";
}
}
AuthController
登录认证接口
/**
* @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<Object> login(User user){
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(user.getUsername(), user.getPassword());
// 登录验证
subject.login(usernamePasswordToken);
// 生成token
String token = jwtTokenProvider.createToken(user.getUsername());
// 将token放入redis,没写
Map<String,Object> authInfo = new HashMap<String,Object>(1){{
put("token", properties.getTokenStartWith() + token);
}};
return ResponseEntity.ok(authInfo);
}
}
ShiroConfiguration
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<String, Filter> 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<Filter> filterRegistrationBean(SecurityManager securityManager, JwtTokenProvider jwtTokenProvider) throws Exception{
FilterRegistrationBean<Filter> filterRegistration = new FilterRegistrationBean<Filter>();
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;
}
}
JwtAuthFilter
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<String, Object> 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;
}
}
DbShiroRealm
登录验证数据源
/**
* @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");
}
}
JwtShiroRealm
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<Role> roles = user.getRoles();
Set<String> roleSet = new HashSet<>();
Set<String> permissionSet = new HashSet<>();
for (Role role : roles) {
roleSet.add(role.getName());
// 将角色下所有权限都存入permissions
Set<Permission> 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;
}
}
JwtToken
为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;
}
}
JwtTokenProvider
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<User> all = userService.findAll();
Map<String, Object> 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 值,可以看到权限的管理功能。
github地址
https://github.com/ebb94f53au/shiro-springboot-demo