項目需求
根據用戶權限不同顯示不同菜單,admin顯示菜單(用戶管理、角色管理、菜單管理),guest顯示菜單(用戶管理、菜單管理)
顯示菜單原理
向客戶端返回不同的菜單數據
需求分析
數據結構:用戶表、角色表、用戶角色關系表、菜單表、菜單角色關系表。
項目業務邏輯:通過登錄用戶名獲取用戶對象,並根據用戶對象的Id屬性獲取菜單信息
技術選型
springboot:主要節省配置時間
springsecurity:用於認證和授權
MySQL:關系型數據管理
mybatis-plus(或mybatis):為了使用自動生成實體類
Lombok:結合mybatis-plus,免去手動編寫實體類的get、set方法
開發前期
數據准備

CREATE TABLE `t_menu` ( `id` INT NOT NULL AUTO_INCREMENT, `name` VARCHAR(30) DEFAULT NULL, `href` VARCHAR(50) DEFAULT NULL, `isEnable` TINYINT(1) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=INNODB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb3; /*Data for the table `t_menu` */ INSERT INTO `t_menu`(`id`,`name`,`href`,`isEnable`) VALUES (1,'用戶管理','users',1), (2,'角色管理','roles',1), (3,'菜單管理','menus',1); /*Table structure for table `t_role` */ CREATE TABLE `t_role` ( `id` INT NOT NULL AUTO_INCREMENT, `role` VARCHAR(50) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=INNODB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb3; /*Data for the table `t_role` */ INSERT INTO `t_role`(`id`,`role`) VALUES (1,'管理員'), (2,'賓客'); /*Table structure for table `t_role_menu` */ CREATE TABLE `t_role_menu` ( `role_Id` INT NOT NULL, `menu_Id` INT NOT NULL, PRIMARY KEY (`role_Id`,`menu_Id`) ) ENGINE=INNODB DEFAULT CHARSET=utf8mb3; /*Data for the table `t_role_menu` */ INSERT INTO `t_role_menu`(`role_Id`,`menu_Id`) VALUES (1,1), (1,2), (1,3), (2,1), (2,3); /*Table structure for table `t_role_user` */ CREATE TABLE `t_role_user` ( `user_Id` INT NOT NULL, `role_Id` INT NOT NULL, PRIMARY KEY (`user_Id`,`role_Id`) ) ENGINE=INNODB DEFAULT CHARSET=utf8mb3; /*Data for the table `t_role_user` */ INSERT INTO `t_role_user`(`user_Id`,`role_Id`) VALUES (1,1), (2,2); /*Table structure for table `t_user` */ CREATE TABLE `t_user` ( `id` INT NOT NULL AUTO_INCREMENT, `username` VARCHAR(50) DEFAULT NULL, `password` VARCHAR(20) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=INNODB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb3; /*Data for the table `t_user` */ INSERT INTO `t_user`(`id`,`username`,`password`) VALUES (1,'admin','123'), (2,'guest','123');
添加依賴

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.2.0</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.4.1</version> </dependency> <dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>
使用mybatis-plus-generator生成基礎架構
這個代碼不是項目代碼所以放在了test下

public class CodeGenerator { // 數據庫 URL private static final String URL = "jdbc:mysql://192.168.223.129:3306/springbootdb?useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC"; // 數據庫驅動 private static final String DRIVER_NAME = "com.mysql.cj.jdbc.Driver"; // 數據庫用戶名 private static final String USERNAME = "root"; // 數據庫密碼 private static final String PASSWORD = "root"; // @author 值 private static final String AUTHOR = "marw"; // 包的基礎路徑 private static final String BASE_PACKAGE_URL = "com.marw.sys"; // xml文件路徑 private static final String XML_PACKAGE_URL = "/src/main/resources/mapper/"; // xml 文件模板 private static final String XML_MAPPER_TEMPLATE_PATH = "/templates/mapper.xml"; // mapper 文件模板 private static final String MAPPER_TEMPLATE_PATH = "/templates/mapper.java"; // entity 文件模板 private static final String ENTITY_TEMPLATE_PATH = "/templates/entity.java"; // service 文件模板 private static final String SERVICE_TEMPLATE_PATH = "/templates/service.java"; // serviceImpl 文件模板 private static final String SERVICE_IMPL_TEMPLATE_PATH = "/templates/serviceImpl.java"; // controller 文件模板 private static final String CONTROLLER_TEMPLATE_PATH = "/templates/controller.java"; public static void main(String[] args) { AutoGenerator generator = new AutoGenerator(); // 全局配置 GlobalConfig globalConfig = new GlobalConfig(); String projectPath = System.getProperty("user.dir"); globalConfig.setOutputDir(projectPath + "/src/main/java"); globalConfig.setAuthor(AUTHOR); globalConfig.setOpen(false); globalConfig.setFileOverride(false); generator.setGlobalConfig(globalConfig); // 數據源配置 DataSourceConfig dataSourceConfig = new DataSourceConfig(); dataSourceConfig.setUrl(URL); dataSourceConfig.setDriverName(DRIVER_NAME); dataSourceConfig.setUsername(USERNAME); dataSourceConfig.setPassword(PASSWORD); generator.setDataSource(dataSourceConfig); // 包配置 PackageConfig packageConfig = new PackageConfig(); //packageConfig.setModuleName("gen"); packageConfig.setParent(BASE_PACKAGE_URL); generator.setPackageInfo(packageConfig); // 自定義配置 InjectionConfig cfg = new InjectionConfig() { @Override public void initMap() { // to do nothing } }; // 如果模板引擎是 freemarker String templatePath = "/templates/mapper.xml.ftl"; // 自定義輸出配置 List<FileOutConfig> focList = new ArrayList<>(); // 自定義配置會被優先輸出 focList.add(new FileOutConfig(templatePath) { @Override public String outputFile(TableInfo tableInfo) { // 自定義輸出文件名 , 如果你 Entity 設置了前后綴、此處注意 xml 的名稱會跟着發生變化!! return projectPath + XML_PACKAGE_URL + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML; } }); cfg.setFileOutConfigList(focList); generator.setCfg(cfg); // 配置自定義代碼模板 TemplateConfig templateConfig = new TemplateConfig(); templateConfig.setXml(null); templateConfig.setMapper(MAPPER_TEMPLATE_PATH); templateConfig.setEntity(ENTITY_TEMPLATE_PATH); templateConfig.setService(SERVICE_TEMPLATE_PATH); templateConfig.setServiceImpl(SERVICE_IMPL_TEMPLATE_PATH); templateConfig.setController(CONTROLLER_TEMPLATE_PATH); generator.setTemplate(templateConfig); // 策略配置 StrategyConfig strategy = new StrategyConfig(); strategy.setNaming(NamingStrategy.underline_to_camel); strategy.setColumnNaming(NamingStrategy.underline_to_camel); strategy.setEntityLombokModel(true); strategy.setRestControllerStyle(true); //strategy.setInclude(scanner("表名")); strategy.setInclude(scanner("表名,多個英文逗號分割").split(",")); //strategy.setSuperEntityColumns("id"); //駝峰轉換 strategy.setControllerMappingHyphenStyle(true); //strategy.setTablePrefix(packageConfig.getModuleName() + "_"); strategy.setTablePrefix("t_"); generator.setStrategy(strategy); generator.setTemplateEngine(new FreemarkerTemplateEngine()); generator.execute(); } private static String scanner(String tip) { Scanner scanner = new Scanner(System.in); System.out.println(("請輸入" + tip + ":")); if (scanner.hasNext()) { String ipt = scanner.next(); if (StringUtils.isNotBlank(ipt)) return ipt; } throw new MybatisPlusException("請輸入正確的" + tip + "!"); } }
生成后的結果也做了修改,效果如下:
基本開發
Mapper
根據用戶名獲取用戶信息

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.marw.sys.mapper.UserMapper"> <select id="findUserByUsername" resultType="com.marw.sys.entity.User"> select * from t_user where username=#{username} </select> </mapper>
根據用戶Id獲取菜單信息,菜單中需要角色信息
@Data public class MenuExtend extends Menu{ private String roleId; private String roleName; }
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.marw.sys.mapper.MenuMapper"> <select id="findMenuByUsreId" resultType="com.marw.sys.entity.MenuExtend"> select tm.*,r.id roleId,r.role roleName from t_menu tm inner join t_role_menu trm on tm.id=trm.menu_Id inner join t_role r on trm.role_id=r.id inner join t_role_user ru on trm.role_Id=ru.role_Id where ru.user_Id=#{id} </select> </mapper>
application.yaml

server: port: 8083 spring: datasource: username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.223.129:3306/springbootdb?useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai&useAffectedRows=true mybatis-plus: type-aliases-package: com.marw.domain mapper-locations: classpath:mapper/*.xml configuration: jdbc-type-for-null: null global-config: banner: false
修改springboot啟動類
添加Mapper包掃描

@SpringBootApplication @MapperScan("com.marw.sys.mapper") public class SpringSecurityWebApplication { public static void main(String[] args) { SpringApplication.run(SpringSecurityWebApplication.class, args); } }
認證開發
springsecurity認證,通過實現UserDetailsService接口中loadUserByUsername方法從數據庫獲取數據對用戶進行認證,認證結果存儲在實現UserDetails接口的對象中,其對象需要包含了用戶信息和菜單信息(菜單信息是我們需要的)
UserDetails實現類SecurityUser

public class SecurityUser implements UserDetails { private User user; private List<MenuExtend> menuList; public User getUser() { return user; } public List<MenuExtend> getMenuList() { return menuList; } public SecurityUser(User user, List<MenuExtend> menuList) { this.user = user; this.menuList = menuList; } /** * 菜單中需要角色信息就是為了這個方法 * @return */ @Override public Collection<? extends GrantedAuthority> getAuthorities() { Collection<GrantedAuthority> authorities = new ArrayList<>(); for (MenuExtend item : this.menuList) { SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(item.getRoleName()); authorities.add(simpleGrantedAuthority); } return authorities; } @Override public String getPassword() { return user.getPassword(); } @Override public String getUsername() { return user.getUsername(); } @Override public boolean isAccountNonExpired() { return true;//數據庫中沒有這個字段直接設置true } @Override public boolean isAccountNonLocked() { return true;//數據庫中沒有這個字段直接設置true } @Override public boolean isCredentialsNonExpired() { return true;//數據庫中沒有這個字段直接設置true } @Override public boolean isEnabled() { return true;//數據庫中沒有這個字段直接設置true } }
UserDetailsService實現類UserDetailsServiceImpl

@Component public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private PasswordEncoder passwordEncoder; @Autowired private UserMapper userMapper; @Autowired private MenuMapper menuMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //根據username獲取user信息 com.marw.sys.entity.User user=userMapper.findUserByUsername(username); if(user==null){ throw new UsernameNotFoundException("用戶不存在"); } user.setPassword(passwordEncoder.encode(user.getPassword())); //根據UserID獲取菜單 List<MenuExtend> menuList = menuMapper.findMenuByUsreId(user.getId()); SecurityUser securityUser = new SecurityUser(user, menuList); return securityUser; } }
設置認證
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsServiceImpl; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //指定認證和加密方式 auth.userDetailsService(userDetailsServiceImpl).passwordEncoder(passwordEncoder()); } @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } }
測試Controller
獲取springsecurity認證的數據是通過SecurityContextHolder獲取的
@RestController public class IndexController { @GetMapping("/index") public String index(){ return "index"; } @GetMapping("/permission") @ResponseBody public Map<String,Object> getPermisiion(){ SecurityUser user = (SecurityUser)SecurityContextHolder.getContext().getAuthentication().getPrincipal(); List<MenuExtend> menuList = user.getMenuList(); Map<String,Object> data = new HashMap<>(); data.put("list",menuList); return data; } }
訪問http://localhost:8083/permission,會跳轉到登陸頁面
通過數據庫中的用戶登錄就可以得到結果: