Широ элегантно интегрируется в SpringBoot со стартером.
Большинство статей, найденных в Интернете, представляют собой методы интеграции в SpringMVC. В этой статье представлены три моих идеи интеграции: 1. Полностью использовать аннотации 2. Полностью использовать конфигурацию URL-адресов 3. Совмещать конфигурацию URL-адресов и аннотаций, конфигурация URL-адресов отвечает за контроль аутентификации, а аннотации отвечают за контроль разрешений. Каждый из трех методов имеет свои преимущества и недостатки, и их необходимо использовать в сценариях практического применения.
код
Talk is cheap, show you my code: elegant-shiro-boot
Этот проект построен с использованием gradle и имеет три подпроекта:
- demo1 демонстрирует, что для аутентификации и авторизации используются только аннотации
- demo2 демонстрирует, что для аутентификации и авторизации используется только конфигурация URL
- demo3 демонстрирует комбинацию двух методов: конфигурация URL отвечает за контроль аутентификации, а конфигурация аннотаций отвечает за контроль авторизации.
Как интегрировать
Пожалуйста, смотрите ссылку на официальном сайте shiro об интеграции springboot с shiro:Integrating Apache Shiro into Spring-Boot Applications
Самое смешное, что я зашел на официальный сайт, чтобы найти его сам, и я не смог найти документацию на этой странице, но нашел ее через google.
Введение в этот документ также довольно простое. Нам просто нужно следовать документации и импортироватьshiro-spring-boot-starter
, а затем внедрить один из наших пользовательских в контейнер SpringRealm
, Широ может знать, как получить информацию о пользователе для обработки в этом мире.鉴权(Authentication)
, как получить информацию о роли пользователя и разрешениях для обработки授权(Authorization)
.
ps: Аутентификацию можно понимать как процесс определения того, вошел ли пользователь в систему, а авторизацию можно понимать как процесс определения наличия прав доступа у вошедшего в систему пользователя.
Процесс интеграции:
1. Представляем стартер.Я использую gradle для сборки проекта, и maven также может ввести соответствующие зависимости:
dependencies {
//spring boot的starter
compile 'org.springframework.boot:spring-boot-starter-web'
compile 'org.springframework.boot:spring-boot-starter-aop'
compile 'org.springframework.boot:spring-boot-devtools'
testCompile 'org.springframework.boot:spring-boot-starter-test'
//shiro
compile 'org.apache.shiro:shiro-spring-boot-web-starter:1.4.0'
}
2. Напишите пользовательскую область
User.java (для других моделей RBAC см. класс в пакете com.abc.entity для кода на github)
public class User {
private Long uid; // 用户id
private String uname; // 登录名,不可改
private String nick; // 用户昵称,可改
private String pwd; // 已加密的登录密码
private String salt; // 加密盐值
private Date created; // 创建时间
private Date updated; // 修改时间
private Set<String> roles = new HashSet<>(); //用户所有角色值,用于shiro做角色权限的判断
private Set<String> perms = new HashSet<>(); //用户所有权限值,用于shiro做资源权限的判断
//getters and setters...
}
UserService.java
@Service
public class UserService {
/**
* 模拟查询返回用户信息
* @param uname
* @return
*/
public User findUserByName(String uname){
User user = new User();
user.setUname(uname);
user.setNick(uname+"NICK");
user.setPwd("J/ms7qTJtqmysekuY8/v1TAS+VKqXdH5sB7ulXZOWho=");//密码明文是123456
user.setSalt("wxKYXuTPST5SG0jMQzVPsg==");//加密密码的盐值
user.setUid(new Random().nextLong());//随机分配一个id
user.setCreated(new Date());
return user;
}
}
RoleService.java
@Service
public class RoleService {
/**
* 模拟根据用户id查询返回用户的所有角色,实际查询语句参考:
* SELECT r.rval FROM role r, user_role ur
* WHERE r.rid = ur.role_id AND ur.user_id = #{userId}
* @param uid
* @return
*/
public Set<String> getRolesByUserId(Long uid){
Set<String> roles = new HashSet<>();
//三种编程语言代表三种角色:js程序员、java程序员、c++程序员
roles.add("js");
roles.add("java");
roles.add("cpp");
return roles;
}
}
PermService.java
@Service
public class PermService {
/**
* 模拟根据用户id查询返回用户的所有权限,实际查询语句参考:
* SELECT p.pval FROM perm p, role_perm rp, user_role ur
* WHERE p.pid = rp.perm_id AND ur.role_id = rp.role_id
* AND ur.user_id = #{userId}
* @param uid
* @return
*/
public Set<String> getPermsByUserId(Long uid){
Set<String> perms = new HashSet<>();
//三种编程语言代表三种角色:js程序员、java程序员、c++程序员
//js程序员的权限
perms.add("html:edit");
//c++程序员的权限
perms.add("hardware:debug");
//java程序员的权限
perms.add("mvn:install");
perms.add("mvn:clean");
perms.add("mvn:test");
return perms;
}
}
CustomRealm.java
/**
* 这个类是参照JDBCRealm写的,主要是自定义了如何查询用户信息,如何查询用户的角色和权限,如何校验密码等逻辑
*/
public class CustomRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Autowired
private RoleService roleService;
@Autowired
private PermService permService;
//告诉shiro如何根据获取到的用户信息中的密码和盐值来校验密码
{
//设置用于匹配密码的CredentialsMatcher
HashedCredentialsMatcher hashMatcher = new HashedCredentialsMatcher();
hashMatcher.setHashAlgorithmName(Sha256Hash.ALGORITHM_NAME);
hashMatcher.setStoredCredentialsHexEncoded(false);
hashMatcher.setHashIterations(1024);
this.setCredentialsMatcher(hashMatcher);
}
//定义如何获取用户的角色和权限的逻辑,给shiro做权限判断
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//null usernames are invalid
if (principals == null) {
throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
}
User user = (User) getAvailablePrincipal(principals);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
System.out.println("获取角色信息:"+user.getRoles());
System.out.println("获取权限信息:"+user.getPerms());
info.setRoles(user.getRoles());
info.setStringPermissions(user.getPerms());
return info;
}
//定义如何获取用户信息的业务逻辑,给shiro做登录
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
String username = upToken.getUsername();
// Null username is invalid
if (username == null) {
throw new AccountException("Null usernames are not allowed by this realm.");
}
User userDB = userService.findUserByName(username);
if (userDB == null) {
throw new UnknownAccountException("No account found for admin [" + username + "]");
}
//查询用户的角色和权限存到SimpleAuthenticationInfo中,这样在其它地方
//SecurityUtils.getSubject().getPrincipal()就能拿出用户的所有信息,包括角色和权限
Set<String> roles = roleService.getRolesByUserId(userDB.getUid());
Set<String> perms = permService.getPermsByUserId(userDB.getUid());
userDB.getRoles().addAll(roles);
userDB.getPerms().addAll(perms);
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(userDB, userDB.getPwd(), getName());
if (userDB.getSalt() != null) {
info.setCredentialsSalt(ByteSource.Util.bytes(userDB.getSalt()));
}
return info;
}
}
3. Используйте аннотацию или конфигурацию URL для управления аутентификацией и авторизацией.
Пожалуйста, обратитесь к примеру на официальном сайте:
//url配置
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
// logged in users with the 'admin' role
chainDefinition.addPathDefinition("/admin/**", "authc, roles[admin]");
// logged in users with the 'document:read' permission
chainDefinition.addPathDefinition("/docs/**", "authc, perms[document:read]");
// all other paths require a logged in user
chainDefinition.addPathDefinition("/**", "authc");
return chainDefinition;
}
//注解配置
@RequiresPermissions("document:read")
public void readDocument() {
...
}
4. Устранить ошибку совместного использования конфигурации Spring AOP и аннотаций. Если вы введете стартер spring aop при использовании конфигурации аннотации shiro, возникнет странная проблема, в результате чего запрос аннотации shiro не может быть сопоставлен, вам необходимо добавить следующую конфигурацию:
@Bean
public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
/**
* setUsePrefix(false)用于解决一个奇怪的bug。在引入spring aop的情况下。
* 在@Controller注解的类的方法中加入@RequiresRole等shiro注解,会导致该方法无法映射请求,导致返回404。
* 加入这项配置能解决这个bug
*/
defaultAdvisorAutoProxyCreator.setUsePrefix(true);
return defaultAdvisorAutoProxyCreator;
}
Идея 1: Используйте аннотации только для управления аутентификацией и авторизацией
Преимущество использования аннотаций заключается в том, что элемент управления детализирован и очень подходит для управления разрешениями на основе ресурсов.
Для управления разрешениями на основе ресурсов рекомендуется прочитать эту статью:The New RBAC: Resource-Based Access Control
Это очень просто с аннотациями. Нам нужно только использовать конфигурацию URL-адреса, чтобы настроить его так, чтобы к пути запроса можно было получить анонимный доступ:
//在 ShiroConfig.java 中的代码:
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition();
// 由于demo1展示统一使用注解做访问控制,所以这里配置所有请求路径都可以匿名访问
chain.addPathDefinition("/**", "anon"); // all paths are managed via annotations
// 这另一种配置方式。但是还是用上面那种吧,容易理解一点。
// or allow basic authentication, but NOT require it.
// chainDefinition.addPathDefinition("/**", "authcBasic[permissive]");
return chain;
}
Затем используйте аннотации, предоставленные shiro в классе контроллера, для управления:
аннотация | Функции |
---|---|
@RequiresGuest | Только туристы могут посетить |
@RequiresAuthentication | Для доступа требуется логин |
@RequiresUser | Вошедшие в систему или "запомнившие меня" пользователи могут получить доступ |
@RequiresRoles | Вошедший в систему пользователь должен иметь указанную роль для доступа |
@RequiresPermissions | Вошедшим в систему пользователям необходимы определенные разрешения для доступа |
Пример кода: (Подробнее см. в demo1 кода github)
/**
* created by CaiBaoHong at 2018/4/18 15:51<br>
* 测试shiro提供的注解及功能解释
*/
@RestController
@RequestMapping("/t1")
public class Test1Controller {
// 由于TestController类上没有加@RequiresAuthentication注解,
// 不要求用户登录才能调用接口。所以hello()和a1()接口都是可以匿名访问的
@GetMapping("/hello")
public String hello() {
return "hello spring boot";
}
// 游客可访问,这个有点坑,游客的意思是指:subject.getPrincipal()==null
// 所以用户在未登录时subject.getPrincipal()==null,接口可访问
// 而用户登录后subject.getPrincipal()!=null,接口不可访问
@RequiresGuest
@GetMapping("/guest")
public String guest() {
return "@RequiresGuest";
}
// 已登录用户才能访问,这个注解比@RequiresUser更严格
// 如果用户未登录调用该接口,会抛出UnauthenticatedException
@RequiresAuthentication
@GetMapping("/authn")
public String authn() {
return "@RequiresAuthentication";
}
// 已登录用户或“记住我”的用户可以访问
// 如果用户未登录或不是“记住我”的用户调用该接口,UnauthenticatedException
@RequiresUser
@GetMapping("/user")
public String user() {
return "@RequiresUser";
}
// 要求登录的用户具有mvn:build权限才能访问
// 由于UserService模拟返回的用户信息中有该权限,所以这个接口可以访问
// 如果没有登录,UnauthenticatedException
@RequiresPermissions("mvn:install")
@GetMapping("/mvnInstall")
public String mvnInstall() {
return "mvn:install";
}
// 要求登录的用户具有mvn:build权限才能访问
// 由于UserService模拟返回的用户信息中【没有】该权限,所以这个接口【不可以】访问
// 如果没有登录,UnauthenticatedException
// 如果登录了,但是没有这个权限,会报错UnauthorizedException
@RequiresPermissions("gradleBuild")
@GetMapping("/gradleBuild")
public String gradleBuild() {
return "gradleBuild";
}
// 要求登录的用户具有js角色才能访问
// 由于UserService模拟返回的用户信息中有该角色,所以这个接口可访问
// 如果没有登录,UnauthenticatedException
@RequiresRoles("js")
@GetMapping("/js")
public String js() {
return "js programmer";
}
// 要求登录的用户具有js角色才能访问
// 由于UserService模拟返回的用户信息中有该角色,所以这个接口可访问
// 如果没有登录,UnauthenticatedException
// 如果登录了,但是没有该角色,会抛出UnauthorizedException
@RequiresRoles("python")
@GetMapping("/python")
public String python() {
return "python programmer";
}
}
Идея 2: Используйте только конфигурацию URL для управления аутентификацией и авторизацией
Широ предоставляет и ряд фильтров по умолчанию, мы можем использовать эти фильтры для настройки разрешений для управления указанным URL-адресом:
аббревиатура конфигурации | соответствующий фильтр | Функции |
---|---|---|
anon | AnonymousFilter | Указанный URL-адрес может быть доступен анонимно |
authc | FormAuthenticationFilter | Указанный URL-адрес требует входа в форму, которая будет получена из запроса по умолчанию.username ,password ,rememberMe Дождитесь параметров и попробуйте войти. Если вы не можете войти, он перейдет на путь, настроенный loginUrl. Мы также можем использовать этот фильтр в качестве логики входа по умолчанию, но обычно мы сами пишем логику входа в контроллер.Если мы пишем ее сами, информацию, возвращаемую по ошибке, можно настроить. |
authcBasic | BasicHttpAuthenticationFilter | Для указания URL-адреса требуется базовый вход в систему |
logout | LogoutFilter | Фильтр выхода, настройте указанный URL-адрес для реализации функции выхода из системы, что очень удобно. |
noSessionCreation | NoSessionCreationFilter | Отключить создание сеанса |
perms | PermissionsAuthorizationFilter | Требуется указанное разрешение на доступ |
port | PortFilter | Необходимо указать порт для доступа |
rest | HttpMethodPermissionFilter | Преобразуйте метод http-запроса в соответствующий глагол, чтобы создать строку разрешения, что не имеет особого смысла. Мне интересно лично посмотреть комментарии к исходному коду. |
roles | RolesAuthorizationFilter | Требуется указанная роль для доступа |
ssl | SslFilter | Требуется HTTPS-запрос для доступа |
user | UserFilter | Для доступа требуется авторизованный пользователь или пользователь с функцией «запомнить меня». |
использовать в пружинном контейнереShiroFilterChainDefinition
для контроля аутентификации и авторизации всех URL-адресов. Преимущество заключается в том, что степень детализации конфигурации велика, а проверка подлинности и авторизация выполняются несколькими контроллерами. Ниже приведен пример, вы можете увидеть демо2 кода github для деталей:
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition();
/**
* 这里小心踩坑!我在application.yml中设置的context-path: /api/v1
* 但经过实际测试,过滤器的过滤路径,是context-path下的路径,无需加上"/api/v1"前缀
*/
//访问控制
chain.addPathDefinition("/user/login", "anon");//可以匿名访问
chain.addPathDefinition("/page/401", "anon");//可以匿名访问
chain.addPathDefinition("/page/403", "anon");//可以匿名访问
chain.addPathDefinition("/t4/hello", "anon");//可以匿名访问
chain.addPathDefinition("/t4/changePwd", "authc");//需要登录
chain.addPathDefinition("/t4/user", "user");//已登录或“记住我”的用户可以访问
chain.addPathDefinition("/t4/mvnBuild", "authc,perms[mvn:install]");//需要mvn:build权限
chain.addPathDefinition("/t4/gradleBuild", "authc,perms[gradle:build]");//需要gradle:build权限
chain.addPathDefinition("/t4/js", "authc,roles[js]");//需要js角色
chain.addPathDefinition("/t4/python", "authc,roles[python]");//需要python角色
// shiro 提供的登出过滤器,访问指定的请求,就会执行登录,默认跳转路径是"/",或者是"shiro.loginUrl"配置的内容
// 由于application-shiro.yml中配置了 shiro:loginUrl: /page/401,返回会返回对应的json内容
// 可以结合/user/login和/t1/js接口来测试这个/t4/logout接口是否有效
chain.addPathDefinition("/t4/logout", "anon,logout");
//其它路径均需要登录
chain.addPathDefinition("/**", "authc");
return chain;
}
Идея 3. Объединение этих двух факторов: конфигурация URL-адреса управляет аутентификацией, а аннотация — авторизацией.
Лично мне очень нравится метод аннотации. Тем не менее, гибкая комбинация двух методов настройки является лучшей практикой для адаптации к различным сценариям применения. Использование только аннотаций или только конфигурации URL потребует утомительной работы.
Я даю два сценария:
Сцена 1
Если я пишу систему управления фоном системы, а мой фон Java — это фон, который просто возвращает данные json, он не будет выполнять работу по переходу на страницу. Тогда наша система фонового управления обычно требует входа в систему для доступа ко всем интерфейсам. Если используются только аннотации, мне нужно добавить на каждый контроллер@RequiresAuthentication
Чтобы объявить, что для доступа к каждому методу в каждом контроллере необходимо войти в систему. Это кажется немного хлопотным, и если вы добавите контроллер в будущем, вам все равно придется добавить эту аннотацию, если вы забудете ее добавить, вы совершите ошибку. В настоящее время вы можете использовать метод конфигурации URL для настройки всех запросов на вход для доступа:chain.addPathDefinition("/**", "authc");
сцена 2
Предположим, я пишу на ресепшн торгового центра, и мой java-бэкенд — это бэкенд, который просто возвращает json-данные, но среди этих интерфейсов под одним и тем же контроллером к некоторым можно получить доступ анонимно, а к некоторым можно получить доступ только войдя в систему. Некоторым требуются определенные роли и разрешения для доступа. Если используется только конфигурация URL-адресов, необходимо настроить каждый URL-адрес, и его легко настроить неправильно, а степень детализации нелегко контролировать.
Итак, мои мысли:用url配置控制鉴权,实现粗粒度控制;用注解控制授权,实现细粒度控制
.
Ниже приведен пример кода (подробности см. в demo3 кода github): ShiroConfig.java
@Configuration
public class ShiroConfig {
//注入自定义的realm,告诉shiro如何获取用户信息来做登录或权限控制
@Bean
public Realm realm() {
return new CustomRealm();
}
@Bean
public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
/**
* setUsePrefix(false)用于解决一个奇怪的bug。在引入spring aop的情况下。
* 在@Controller注解的类的方法中加入@RequiresRole注解,会导致该方法无法映射请求,导致返回404。
* 加入这项配置能解决这个bug
*/
creator.setUsePrefix(true);
return creator;
}
/**
* 这里统一做鉴权,即判断哪些请求路径需要用户登录,哪些请求路径不需要用户登录。
* 这里只做鉴权,不做权限控制,因为权限用注解来做。
* @return
*/
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition();
//哪些请求可以匿名访问
chain.addPathDefinition("/user/login", "anon");
chain.addPathDefinition("/page/401", "anon");
chain.addPathDefinition("/page/403", "anon");
chain.addPathDefinition("/t5/hello", "anon");
chain.addPathDefinition("/t5/guest", "anon");
//除了以上的请求外,其它请求都需要登录
chain.addPathDefinition("/**", "authc");
return chain;
}
}
PageController.java
@RestController
@RequestMapping("/page")
public class PageController {
// shiro.loginUrl映射到这里,我在这里直接抛出异常交给GlobalExceptionHandler来统一返回json信息,
// 您也可以在这里json,不过这样子就跟GlobalExceptionHandler中返回的json重复了。
@RequestMapping("/401")
public Json page401() {
throw new UnauthenticatedException();
}
// shiro.unauthorizedUrl映射到这里。由于demo3统一约定了url方式只做鉴权控制,不做权限访问控制,
// 也就是说在ShiroConfig中如果没有roles[js],perms[mvn:install]这样的权限访问控制配置的话,
// 是不会跳转到这里的。
@RequestMapping("/403")
public Json page403() {
throw new UnauthorizedException();
}
@RequestMapping("/index")
public Json pageIndex() {
return new Json("index",true,1,"index page",null);
}
}
GlobalExceptionHandler.java
/**
* 统一捕捉shiro的异常,返回给前台一个json信息,前台根据这个信息显示对应的提示,或者做页面的跳转。
*/
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
//不满足@RequiresGuest注解时抛出的异常信息
private static final String GUEST_ONLY = "Attempting to perform a guest-only operation";
@ExceptionHandler(ShiroException.class)
@ResponseBody
public Json handleShiroException(ShiroException e) {
String eName = e.getClass().getSimpleName();
log.error("shiro执行出错:{}",eName);
return new Json(eName, false, Codes.SHIRO_ERR, "鉴权或授权过程出错", null);
}
@ExceptionHandler(UnauthenticatedException.class)
@ResponseBody
public Json page401(UnauthenticatedException e) {
String eMsg = e.getMessage();
if (StringUtils.startsWithIgnoreCase(eMsg,GUEST_ONLY)){
return new Json("401", false, Codes.UNAUTHEN, "只允许游客访问,若您已登录,请先退出登录", null)
.data("detail",e.getMessage());
}else{
return new Json("401", false, Codes.UNAUTHEN, "用户未登录", null)
.data("detail",e.getMessage());
}
}
@ExceptionHandler(UnauthorizedException.class)
@ResponseBody
public Json page403() {
return new Json("403", false, Codes.UNAUTHZ, "用户没有访问权限", null);
}
}
TestController.java
@RestController
@RequestMapping("/t5")
public class Test5Controller {
// 由于ShiroConfig中配置了该路径可以匿名访问,所以这接口不需要登录就能访问
@GetMapping("/hello")
public String hello() {
return "hello spring boot";
}
// 如果ShiroConfig中没有配置该路径可以匿名访问,所以直接被登录过滤了。
// 如果配置了可以匿名访问,那这里在没有登录的时候可以访问,但是用户登录后就不能访问
@RequiresGuest
@GetMapping("/guest")
public String guest() {
return "@RequiresGuest";
}
@RequiresAuthentication
@GetMapping("/authn")
public String authn() {
return "@RequiresAuthentication";
}
@RequiresUser
@GetMapping("/user")
public String user() {
return "@RequiresUser";
}
@RequiresPermissions("mvn:install")
@GetMapping("/mvnInstall")
public String mvnInstall() {
return "mvn:install";
}
@RequiresPermissions("gradleBuild")
@GetMapping("/gradleBuild")
public String gradleBuild() {
return "gradleBuild";
}
@RequiresRoles("js")
@GetMapping("/js")
public String js() {
return "js programmer";
}
@RequiresRoles("python")
@GetMapping("/python")
public String python() {
return "python programmer";
}
}