权限配置表如下
menu表
menu_role表
role表
user表
user_role表
五张表构成一个基本的权限管理
表的解释如下:
- menu:菜单资源表,把需要管理的访问路径配置在这张表里
- menu_role:菜单资源权限表,表示访问此路径需要什么权限
- role:角色权限表
- user:用户表,记录一些基本信息
- user_role:用户角色对应表,表示一个用户都有哪些角色
pom.xml
<!--WEB依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
<version>5.1.27</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!--spring-cache-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!--redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<build>
<!--如果mybatis的Mapper文件要和接口写在一起,记得配置一下资源-->
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
application.properties
#mysql
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.url=jdbc:mysql://localhost/javaboy2?characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=root
#redis
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.database=0
#在启动时创建缓存名称,即前面的cacheNames,多个名称用逗号分隔。
spring.cache.cache-names=c1
#十分钟没再使用缓存就清空
spring.cache.redis.time-to-live=600000
启动类
@SpringBootApplication
@MapperScan(basePackages = "com.javaboy.securitydy.mapper")
@EnableCaching
public class SecurityDyApplication {
public static void main(String[] args) {
SpringApplication.run(SecurityDyApplication.class, args);
}
}
在bean包下创建三个实体类
User
public class User implements UserDetails {
private Integer id;
private String username;
private String password;
private Boolean enabled;
private Boolean locked;
private List<Role> roles;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (Role role : roles) {
//如果数据库角色不是以ROLE_开头,那么这里要加上,不然会找不到对应权限
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
//账户是否未过期
@Override
public boolean isAccountNonExpired() {
return true;
}
//账户是否未锁定
@Override
public boolean isAccountNonLocked() {
return !locked;
}
//凭证是否未过期
@Override
public boolean isCredentialsNonExpired() {
return true;
}
//账户是否可用
@Override
public boolean isEnabled() {
return enabled;
}
//省略getter/setter
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
// public Boolean getEnabled() {
// return enabled;
// }
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
public Boolean getLocked() {
return locked;
}
public void setLocked(Boolean locked) {
this.locked = locked;
}
public List<Role> getRoles() {
return roles;
}
public void setRoles(List<Role> roles) {
this.roles = roles;
}
}
至于User为什么要实现UserDetails是因为在后面使用security登录接口时要实现一个方法,这个方法需要UserDetails返回
Role
public class Role implements Serializable {
private Integer id;
private String name;
private String nameZh;
//省略getter/setter
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getNameZh() {
return nameZh;
}
public void setNameZh(String nameZh) {
this.nameZh = nameZh;
}
实现Serializable 是为了后面用springcache缓存接口数据
Menu
public class Menu implements Serializable {
private Integer id;
private String pattern;
private List<Role> roles;
public List<Role> getRoles() {
return roles;
}
public void setRoles(List<Role> roles) {
this.roles = roles;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getPattern() {
return pattern;
}
public void setPattern(String pattern) {
this.pattern = pattern;
}
下面在service包下创建 UserService
@Service
public class UserService implements UserDetailsService {
@Autowired
UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.loadUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("账户不存在!");
}
user.setRoles(userMapper.getUserRolesByUid(user.getId()));
return user;
}
}
userMapper
public interface UserMapper {
User loadUserByUsername(String username);
List<Role> getUserRolesByUid(Integer id);
}
xml
<?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.javaboy.securitydy.mapper.UserMapper">
<select id="loadUserByUsername" resultType="com.javaboy.securitydy.bean.User">
select * from user where username=#{username}
</select>
<select id="getUserRolesByUid" resultType="com.javaboy.securitydy.bean.Role">
select * from role r,user_role ur where r.id=ur.rid and ur.uid=#{id}
</select>
</mapper>
根据用户名去查如果查不到抛出异常,登录失败
如果查到了 就接着去查用户有哪些权限一并返回到下面config
下面做处理
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserService userService;
//去查找有没有对应的用户和权限
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService);
}
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("admin")
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginProcessingUrl("/doLogin")
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
Map<String,Object> map = new HashMap<>();
map.put("status",200);
//登录成功的用户对象
map.put("msg",authentication.getPrincipal());
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
}
})
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
Map<String,Object> map = new HashMap<>();
map.put("status",401);
if (e instanceof LockedException){
map.put("msg","账户被锁定,登录失败!");
}else if (e instanceof BadCredentialsException){
map.put("msg","用户或密码输入错误,登录失败!");
}else if (e instanceof DisabledException){
map.put("msg","账户被禁用,登录失败!");
}else if (e instanceof AccountExpiredException){
map.put("msg","账户过期,登录失败!");
}else if (e instanceof CredentialsExpiredException){
map.put("msg","密码过期,登录失败!");
}else {
map.put("msg","登录失败!");
}
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
}
})
.permitAll()
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
Map<String,Object> map = new HashMap<>();
map.put("status",200);
//登录成功的用户对象
map.put("msg","注销登录成功!");
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
}
})
.and()
.csrf().disable();
}
以上配置完成后 咱们打开浏览器测试一下
输入:http://localhost:8080/login
发现已经登录成功!
到现在已经成功实现基于数据认证登录
下面就开始实现基于数据库动态权限配置
配置以下类
MyFilter
@Component
public class MyFilter implements FilterInvocationSecurityMetadataSource {
AntPathMatcher pathMatcher = new AntPathMatcher();
@Autowired
MenuService menuService;
//根据请求地址分析出需要哪些角色
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
//获取请求地址 例:/admin/**
String requestUrl = ((FilterInvocation)o).getRequestUrl();
List<Menu> allMenus = menuService.getAllMenus();
for (Menu menu : allMenus) {
if (pathMatcher.match(menu.getPattern(),requestUrl)){
List<Role> roles = menu.getRoles();
String[] rolesStr = new String[roles.size()];
for (int i = 0; i < roles.size(); i++) {
rolesStr[i] = roles.get(i).getName();
}
return SecurityConfig.createList(rolesStr);
}
}
return SecurityConfig.createList("ROLE_login");
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
MenuService
@Service
@CacheConfig(cacheNames = "c1")
public class MenuService {
@Autowired
MenuMapper menuMapper;
//Collection检查是否有访问权限时
//每次都要查询数据库,完全没必要,权限这块没有那么频繁的更新
//这里使用缓存进行,加快查询效率
@Cacheable
public List<Menu> getAllMenus(){
System.out.println("getAllMenus方法进来了");
return menuMapper.getAllMenus();
}
}
MenuMapper
public interface MenuMapper {
List<Menu> getAllMenus();
}
xml
<?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.javaboy.securitydy.mapper.MenuMapper">
<resultMap id="BaseResultMap" type="com.javaboy.securitydy.bean.Menu">
<id property="id" column="id"/>
<result property="pattern" column="pattern"/>
<collection property="roles" ofType="com.javaboy.securitydy.bean.Role">
<id column="rid" property="id"/>
<result column="rname" property="name"/>
<result column="rnameZh" property="nameZh"/>
</collection>
</resultMap>
<select id="getAllMenus" resultMap="BaseResultMap">
SELECT m.*,r.`id` AS rid,r.`name` AS rname,r.`nameZh` AS rnameZh
FROM menu m LEFT JOIN menu_role mr ON m.`id`=mr.`mid` LEFT JOIN role r ON mr.`rid`=r.`id`
</select>
</mapper>
上面配置完毕后,再来配置最后一个
@Component
public class MyAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
for (ConfigAttribute attribute : collection) {
if ("ROLE_login".equals(attribute.getAttribute())){
//未登录,匿名请求
if (authentication instanceof AnonymousAuthenticationToken){
throw new AccessDeniedException("非法请求!");
}else {
return;
}
}
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
if (authority.getAuthority().equals(attribute.getAttribute())){
return;
}
}
//全匹配完也没找到对应的
throw new AccessDeniedException("非法请求!");
}
}
//是否支持这种方式
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
- MyFilter配置里通过
menuService.getAllMenus()
获取了所有角色对应路径的关系 - 然后通过路径匹配
pathMatcher.match(menu.getPattern(),requestUrl)
循环进行匹配 - 如果匹配到了就返回当前路径需要的角色
- 没有则返回一个默认的角色
ROLE_login
- 返回后会在
MyAccessDecisionManager
配置类里获取到刚才返回的信息 - 匹配权限看是否能找到,找不到则抛出异常 登录失败
最后在WebSecurityConfig
配置一下就可以了
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserService userService;
@Autowired
MyFilter myFilter;
@Autowired
MyAccessDecisionManager myAccessDecisionManager;
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
//角色继承
@Bean
RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
String hierarchy = "ROLE_dba > ROLE_admin \n ROLE_admin > ROLE_user";
roleHierarchy.setHierarchy(hierarchy);
return roleHierarchy;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O o) {
o.setAccessDecisionManager(myAccessDecisionManager);
o.setSecurityMetadataSource(myFilter);
return o;
}
})
.and()
.formLogin()
.permitAll()
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
Map<String,Object> map = new HashMap<>();
map.put("status",200);
//登录成功的用户对象
map.put("msg","注销登录成功!");
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
}
})
.and()
.csrf().disable();
经过以上配置就可以实现动态权限配置了
下面写接口测试就行了
如下:
@RestController
public class HelloController {
@GetMapping("/admin/hello")
public String admin() {
return "hello admin";
}
@GetMapping("/db/hello")
public String dba() {
return "hello dba";
}
@GetMapping("/user/hello")
public String user() {
return "hello user";
}
@GetMapping("/qp/hello")
public String qp() {
return "hello qp";
}
@GetMapping("/hello")
public String hello(){
return "hello";
}
}