基于SpringBoot 开发Restful风格的API
代码以上传码云 https://gitee.com/HuiSeChengXuYuan/shiro-demo
1.基于Spring boot 2.1.3 开发
2.接口文档使用Swagger
3.权限控制框架使用Shiro
Maven 依赖:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.2</version>
</dependency>
<!--swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.4.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.2.4</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Shiro介绍
Shiro是一个强大易用的Java安全框架,提供了认证、授权、加密和会话管理等功能。
Authentication: 身份认证/登录,验证用户是不是拥有相应的身份;
Authorization: 授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
Session Manager: 会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;
Cryptography: 加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
Web Support: Web支持,可以非常容易的集成到Web环境;
**Caching:**缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;
Concurrency: shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
Testing: 提供测试支持;
Run As: 允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
Remember Me: 记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。
记住一点,Shiro不会去维护用户、维护权限;这些需要我们自己去设计/提供;然后通过相应的接口注入给Shiro即可。
shiro鉴权过程
首先,我们从外部来看Shiro吧,即从应用程序角度的来观察如何使用Shiro完成工作。如下图:
可以看到:应用代码直接交互的对象是Subject,也就是说Shiro的对外API核心就是Subject;其每个API的含义:
Subject:主体,代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;即一个抽象概念;所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager;可以把Subject认为是一个门面;SecurityManager才是实际的执行者;
SecurityManager:安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject;可以看出它是Shiro的核心,它负责与后边介绍的其他组件进行交互,如果学习过SpringMVC,你可以把它看成DispatcherServlet前端控制器;
Realm:域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。
shiro架构
Subject:主体,可以看到主体可以是任何可以与应用交互的“用户”;
SecurityManager:相当于SpringMVC中的DispatcherServlet或者Struts2中的FilterDispatcher;是Shiro的心脏;所有具体的交互都通过SecurityManager进行控制;它管理着所有Subject、且负责进行认证和授权、及会话、缓存的管理。
Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得Shiro默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
Authrizer:授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;
Realm:可以有1个或多个Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是JDBC实现,也可以是LDAP实现,或者内存实现等等;由用户提供;注意:Shiro不知道你的用户/权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的Realm;
SessionManager:如果写过Servlet就应该知道Session的概念,Session呢需要有人去管理它的生命周期,这个组件就是SessionManager;而Shiro并不仅仅可以用在Web环境,也可以用在如普通的JavaSE环境、EJB等环境;所有呢,Shiro就抽象了一个自己的Session来管理主体与应用之间交互的数据;这样的话,比如我们在Web环境用,刚开始是一台Web服务器;接着又上了台EJB服务器;这时想把两台服务器的会话数据放到一个地方,这个时候就可以实现自己的分布式会话(如把数据放到Memcached服务器);
SessionDAO:DAO大家都用过,数据访问对象,用于会话的CRUD,比如我们想把Session保存到数据库,那么可以实现自己的SessionDAO,通过如JDBC写到数据库;比如想把Session放到Memcached中,可以实现自己的Memcached SessionDAO;另外SessionDAO中可以使用Cache进行缓存,以提高性能;
CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能
Cryptography:密码模块,Shiro提高了一些常见的加密组件用于如密码加密/解密的。
废话不多说直接上代码
项目结构:
Swagger 配置类
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Value("${swagger.show}")
private Boolean enable;
@Bean
public Docket swaggerSpringMvcPlugin() {
//在请求头中添加一个为 Authorization 的属性 shiro拦截器 认证的时候会用到
Parameter parameter = new ParameterBuilder()
.name("token")
.description("token")
.modelRef(new ModelRef("string"))
.parameterType("header")
.required(false)
.build();
return new Docket(DocumentationType.SWAGGER_2)
.enable(enable)
.groupName("demo")
.apiInfo(apiInfo())
.globalOperationParameters(Collections.singletonList(parameter))
.select()
.apis(withClassAnnotation(Api.class))
.paths(this.paths())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("Shiro集成")
.description("接口")
.version("1.0")
.build();
}
public static Predicate<String> paths() {
return Predicates.alwaysTrue();
}
}
Spring MVC 全局异常处理类
@RestControllerAdvice
public class GlobalControllerExceptionHandler {
@ResponseStatus(HttpStatus.UNAUTHORIZED)//response状态码
@ExceptionHandler(AuthorizationException.class)//捕捉的Shiro异常
public Result AuthorizationExceptionHandler(AuthorizationException e) {
Result result = new Result(String.valueOf(HttpStatus.UNAUTHORIZED.value()), "权限不足");
return result;
}
}
Result类
@Data
public class Result {
public Result(String code, String message) {
this.code = code;
this.message = message;
}
//错误编码
private String code;
//错误信息
private String message;
}
ShiroConfig类配置
@Configuration
public class ShiroConfig {
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);//设置 securityManager
Map<String, String> filterChainDefinitionMapping = shiroFilter.getFilterChainDefinitionMap();
setUrl(filterChainDefinitionMapping, "anon", AnonUrl());//不需要拦截的路径
setUrl(filterChainDefinitionMapping, "method,auth", AuthUrl());//需要被拦截的路径 这里的 method、auth 和下面的拦截器对应
Map<String, Filter> filterMap = new HashMap<>();
//MethodFilter TokenFilter 自定义拦截器
filterMap.put("method", new MethodFilter());// cors自己随便定义,写abc也行 个人喜欢
filterMap.put("auth", new TokenFilter());// auth自己随便定义,写abc也行 个人喜欢
shiroFilter.setFilters(filterMap);
return shiroFilter;
}
private void setUrl(Map<String, String> filterChainDefinitionMapping, String filterName, List<String> urlList) {
if (!urlList.isEmpty()) {
Iterator<String> iterator = urlList.iterator();
while (iterator.hasNext()) {
String url = iterator.next();
filterChainDefinitionMapping.put(url, filterName);//
}
}
}
//不对swagger访问路径拦截
private List<String> AnonUrl() {
List<String> list = new ArrayList<>();
list.add("/swagger*");
list.add("/api/test/login");
return list;
}
//对拦截 /api/** 的API进行拦截
private List<String> AuthUrl() {
List<String> list = new ArrayList<>();
list.add("/api/**");
return list;
}
@Bean("securityManager")
public SecurityManager securityManager(TokenRealm tokenRealm) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm(tokenRealm);//设置域对象
DefaultSubjectDAO de = (DefaultSubjectDAO) manager.getSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = (DefaultSessionStorageEvaluator) de.getSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);//禁用Session存储
StatelessDefaultSubjectFactory statelessDefaultSubjectFactory = new StatelessDefaultSubjectFactory();
manager.setSubjectFactory(statelessDefaultSubjectFactory);
manager.setSessionManager(this.defaultSessionManager());
SecurityUtils.setSecurityManager(manager);
return manager;
}
@Bean("tokenRealm")
public TokenRealm tokenRealm() {
return new TokenRealm();
}
@Bean
public DefaultSessionManager defaultSessionManager() {
DefaultSessionManager manager = new DefaultSessionManager();
manager.setSessionValidationSchedulerEnabled(false);//禁用Session
return manager;
}
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public AuthorizationAttributeSourceAdvisor advisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
//启用Shiro注解
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
}
MethodFilter
public class MethodFilter extends AbstractFilter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
System.out.println("请求方法 Method : " + request.getMethod());
filterChain.doFilter(servletRequest,servletResponse);
}
}
TokenFilter
//需要认证的API被调用前执行的拦截器
public class TokenFilter extends AuthenticationFilter {
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
String token = getToken(servletRequest);
if (StringUtils.isEmpty(token)) {
return false;
} else {
boolean isSuccess = this.login(token);
if (!isSuccess) {
this.printUnauthorized("401", (HttpServletResponse) servletResponse);
}
return isSuccess;
}
}
private boolean login(String token) {
try {
Subject subject = SecurityUtils.getSubject();
subject.login(new Token(token));//subject.login() 调用 自定义的 TokenRealm 对象,进行认证和授权。
return true;
} catch (AuthenticationException e) {
// e.printStackTrace();
return false;
}
}
private String getToken(ServletRequest servletRequest) {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String authorizationHeader = request.getHeader("token");//获取请求头中的Authorization属性
if (!StringUtils.isEmpty(authorizationHeader)) {
return authorizationHeader.replace(" ", "");
}
return null;
}
private void printUnauthorized(String messageCode, HttpServletResponse response) {
String content = String.format("{\"code\":\"%s\",\"msg\":\"%s\"}", messageCode, HttpStatus.UNAUTHORIZED.getReasonPhrase());
response.setContentType("application/json;charset=UTF-8");
response.setContentLength(content.length());
response.setStatus(HttpStatus.UNAUTHORIZED.value());
try {
PrintWriter writer = response.getWriter();
writer.write(content);
} catch (IOException var5) {
var5.printStackTrace();
}
}
}
Token
//自定义Token 实现AuthenticationToken 认证是需要用到
public class Token implements AuthenticationToken {
private String token;
Token(String token) {
this.token = token;
}
String getToken() {
return this.token;
}
@Override
public Object getPrincipal() {
return this.getToken();
}
@Override
public Object getCredentials() {
return this.getToken();
}
}
StatelessDefaultSubjectFactory
//subject工厂
public class StatelessDefaultSubjectFactory extends DefaultWebSubjectFactory {
public Subject createSubject(SubjectContext context) {
context.setSessionCreationEnabled(false);//禁用Session
return super.createSubject(context);
}
}
TokenRealm
//自定义域对象,实现认证和授权的方法
public class TokenRealm extends AuthorizingRealm {
@Resource(name = "stringRedisTemplate")
private ValueOperations<String, String> redis;
@Resource
private StringRedisTemplate stringRedisTemplate;
public String getName() {
return "Realm";
}
@Override
public boolean supports(AuthenticationToken token) {
return token != null && Token.class.isAssignableFrom(token.getClass());
}
//认证 Authentication
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("认证 Authentication");
Token token = (Token) authenticationToken;
String role = redis.get("random.token." + token.getToken());//获取对应token的角色
if (!StringUtils.isEmpty(role)) {
return new SimpleAuthenticationInfo(role, token.getToken(), this.getName());
}
return null;
}
//授权 Authorization
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
System.out.println("授权 Authorization");
String role = (String) principal.getPrimaryPrincipal();
if (!StringUtils.isEmpty(role)) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addRole(role);
return info;
}
return null;
}
}
TestController
@RestController
@RequestMapping("/api/test")
@Api(tags = "测试模块")
public class TestController {
@Resource(name = "stringRedisTemplate")
private ValueOperations<String, String> redis;
@Resource
private StringRedisTemplate stringRedisTemplate;
private String[] tokens = {"token1", "token2", "token3"};
private String[] roles = {"admin", "user"};
@ApiOperation("测试")
@GetMapping("/getAdmin")
@RequiresRoles(logical = Logical.OR, value = {"admin"})//shiro注解 admin角色
public String getAdmin() {
return "Admin测试成功";
}
@ApiOperation("测试")
@GetMapping("/getUser")
@RequiresRoles(logical = Logical.OR, value = {"user"})//shiro注解 user角色
public String getUser() {
return "User测试成功";
}
@ApiOperation("登录")
@PostMapping("/login")
public Map<String, String> login(@RequestParam @ApiParam String name, @RequestParam @ApiParam String password) {
String token = "";
String role = "";
if (name.equals("user") && password.equals("123456")) {
Random random = new Random();
token = tokens[random.nextInt(tokens.length)];//随机token ,开发项目可用随机字符替代
role = roles[random.nextInt(roles.length)];//随机角色
redis.set("random.token." + token, role); //将token和token对应的role存入 Redis中
Map<String, String> map = new HashMap<>();
map.put("token", token);
map.put("role", role);
return map;
}
return null;
}
}
application.yml
spring:
application:
name: demo
redis:
host: 127.0.0.1
port: 6379
password: 123456
database: 1
timeout: 5000ms
jedis:
pool:
min-idle: 1
max-idle: 1
max-active: 2
max-wait: -1ms
server:
port: 80
swagger:
show: true
测试效果
参考文章
- 作者:clj198606061111