什么是Spring Security?
我就只说下SpringSecurity核心功能:
认证(你是谁)
授权(你能干什么)
准备工作
在数据库中创建5张表
user用户表
menu资源表
role权限表
user_role用户权限表
menu_role资源权限表
用户表User
public class User implements UserDetails {
private Integer id;
private String name;
private String phone;
private String address;
private String username;
private String password;
public List<Role> getRoles() {
return roles;
}
public void setRoles(List<Role> roles) {
this.roles = roles;
}
private List<Role> roles;
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 == null ? null : name.trim();
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone == null ? null : phone.trim();
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address == null ? null : address.trim();
}
public String getUsername() {
return username;
}
// 帐户是否过期
@Override
public boolean isAccountNonExpired() {
return false;
}
// 帐户是否被冻结
@Override
public boolean isAccountNonLocked() {
return false;
}
// 帐户密码是否过期,一般有的密码要求性高的系统会使用到,比较每隔一段时间就要求用户重置密码
@Override
public boolean isCredentialsNonExpired() {
return false;
}
// 帐号是否可用
@Override
public boolean isEnabled() {
return false;
}
public void setUsername(String username) {
this.username = username == null ? null : username.trim();
}
// 封装了权限信息
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authorities = new ArrayList<>();
for (Role role : roles) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return authorities;
}
// 密码信息
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password == null ? null : password.trim();
}
}
url资源路径表Menu
public class Menu {
private Integer id;
private String url;
public List<Role> getRoles() {
return roles;
}
public void setRoles(List<Role> roles) {
this.roles = roles;
}
private List<Role> roles;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url == null ? null : url.trim();
}
}
权限表Role
public class Role {
private Integer id;
private String name;
private String namezh;
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 == null ? null : name.trim();
}
public String getNamezh() {
return namezh;
}
public void setNamezh(String namezh) {
this.namezh = namezh == null ? null : namezh.trim();
}
}
1。pom添加依赖
<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>
<!-- Spring Boot Redis依赖 -->
<!-- 注意:1.5版本的依赖和2.0的依赖不一样,注意看哦 1.5我记得名字里面应该没有“data”, 2.0必须是“spring-boot-starter-data-redis” 这个才行-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 缓存: spring cache -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
2。application.yml
server:
port: 8081
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://localhost::3306/lcy?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
redis:
host: 127.0.0.1
database: 0
port: 6379
password: 123
cache:
cache-names: menus_cache
3。配置Security
1_在config文件夹中创建WebSecurityConfig
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启security注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserService userService;
@Autowired
MyAccessDecisionManager myAccessDecisionManager;
@Autowired
MyInvocationSecurityMetadataSourceService myInvocationSecurityMetadataSourceService;
// 用户认证配置
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 指定用户认证时,默认从哪里获取认证用户信息
auth.userDetailsService(userService);
}
// 密码加密器
@Bean
public PasswordEncoder passwordEncoder() {
/**
* BCryptPasswordEncoder:相同的密码明文每次生成的密文都不同,安全性更高
*/
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//authorizeRequests() 开始请求权限配置
//antMatchers() 使用Ant风格的路径匹配,这里配置匹配 / 和 /index
//permitAll() 用户可任意访问
//anyRequest() 匹配所有路径
//authenticated() 用户登录后可访问
http.authorizeRequests() // 定义哪些URL需要被保护、哪些不需要被保护
// .anyRequest().authenticated()
//请求过滤类 MyFilterSecurityInterceptor
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {//ObjectPostProcessor 概念,用来修改或替换Java配置的对象实例
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setAccessDecisionManager(myAccessDecisionManager);//资源权限认证器 认证用户是否拥有所请求资源的权限
object.setSecurityMetadataSource(myInvocationSecurityMetadataSourceService);//加载资源与权限的对应关系
return object;
}
})
.and()
.formLogin()
.usernameParameter("username")//登录表单form中用户名输入框input的name名,不修改的话默认是username
.passwordParameter("password")//form中密码输入框input的name名,不修改的话默认是password
.loginProcessingUrl("/doLogin")//登录表单form中action的地址,也就是处理认证请求的路径
.loginPage("/login")//用户未登录时,访问任何资源都转跳到该路径,即登录页面
.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();
//在认证模块通过token获取用户信息
User user = (User) authentication.getPrincipal();
user.setPassword(null);//清空用户密码
RespBean ok = RespBean.ok("登录成功!", user);
String s = new ObjectMapper().writeValueAsString(ok);
out.write(s);
out.flush();
out.close();
}
})
.failureHandler(new AuthenticationFailureHandler() {//自定义登录失败模块
@Override
public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException exception) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
RespBean respBean = RespBean.error("登录失败!");
if (exception instanceof LockedException) {
respBean.setMsg("账户被锁定,请联系管理员!");
} else if (exception instanceof CredentialsExpiredException) {
respBean.setMsg("密码过期,请联系管理员!");
} else if (exception instanceof AccountExpiredException) {
respBean.setMsg("账户过期,请联系管理员!");
} else if (exception instanceof DisabledException) {
respBean.setMsg("账户被禁用,请联系管理员!");
} else if (exception instanceof BadCredentialsException) {
respBean.setMsg("用户名或者密码输入错误,请重新输入!");
}
out.write(new ObjectMapper().writeValueAsString(respBean));
out.flush();
out.close();
}
})
.permitAll()
.and()
.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();
out.write(new ObjectMapper().writeValueAsString(RespBean.ok("注销成功!")));
out.flush();
out.close();
}
})
.permitAll()//注销行为任意访问
.and()
.csrf().disable()//禁用跨站csrf攻击防御
.exceptionHandling()//AuthenticationEntryPoint 用来解决匿名用户访问无权限资源时的异常
.authenticationEntryPoint(new AuthenticationEntryPoint() {
@Override
public void commence(HttpServletRequest req, HttpServletResponse resp, AuthenticationException authException) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
resp.setStatus(401);
PrintWriter out = resp.getWriter();
RespBean respBean = RespBean.error("访问失败!");
if (authException instanceof InsufficientAuthenticationException) {
respBean.setMsg("请求失败,请联系管理员!");
}
out.write(new ObjectMapper().writeValueAsString(respBean));
out.flush();
out.close();
}
});
}
}
2_MyAccessDecisionManager 资源权限认证器 认证用户是否拥有所请求资源的权限
/***
* @FileName: MyAccessDecisionManager
* @remark: 资源权限认证器 证用户是否拥有所请求资源的权限
* @explain 接口AccessDecisionManager也是必须实现的。 decide方法里面写的就是授权策略了,需要什么策略,可以自己写其中的策略逻辑
* 认证通过就返回,不通过抛异常就行了,spring security会自动跳到权限不足处理类(WebSecurityConfig 类中 配置文件上配的)
*/
@Component
public class MyAccessDecisionManager implements AccessDecisionManager {
/**
* 授权策略
* decide()方法在url请求时才会调用,服务器启动时不会执行这个方法
* @param collection 装载了请求的url允许的角色数组 。这里是从MyInvocationSecurityMetadataSource里的loadResourceDefine方法里的atts对象取出的角色数据赋予给了collection对象
* @param o url
* @param authentication 装载了从数据库读出来的权限(角色) 数据。这里是从MyUserDetailService里的loadUserByUsername方法里的grantedAuths对象的值传过来给 authentication 对象,简单点就是从spring的全局缓存SecurityContextHolder中拿到的,里面是用户的权限信息
*
* 注意: Authentication authentication 如果是前后端分离 则有跨域问题,跨域情况下 authentication 无法获取当前登陆人的身份认证(登陆成功后),我尝试用token来效验权限
*/
@Override
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
for (ConfigAttribute configAttribute : collection) {
String needRole = configAttribute.getAttribute();//获取请求的url允许的权限数组
if ("ROLE_LOGIN".equals(needRole)) {//该路径需要登录
if (authentication instanceof AnonymousAuthenticationToken) {
throw new AccessDeniedException("尚未登录,请登录!");
}else {
return;
}
}
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();//获取该用户权限信息
for (GrantedAuthority authority : authorities) {//将用户的权限与url所需权限匹配。
//grantedAuthority 为用户所被赋予的权限。 needRole 为访问相应的资源应该具有的权限。
//判断两个请求的url的权限和用户具有的权限是否相同,如相同,允许访问 权限就是那些以ROLE_为前缀的角色
if (authority.getAuthority().equals(needRole)) { //匹配到对应的角色,则允许通过
return;
}
}
}
throw new AccessDeniedException("权限不足,请联系管理员!");
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
3_MyInvocationSecurityMetadataSourceService 加载资源与权限的对应关系
/***
* @FileName: MyInvocationSecurityMetadataSourceService
* @remark: 加载资源与权限的对应关系
* @explain 实现FilterInvocationSecurityMetadataSource接口也是必须的。 首先,这里从数据库中获取信息。 其中loadResourceDefine方法不是必须的,
* 这个只是加载所有的资源与权限的对应关系并缓存起来,避免每次获取权限都访问数据库(提高性能),然后getAttributes根据参数(被拦截url)返回权限集合。
* 这种缓存的实现其实有一个缺点,因为loadResourceDefine方法是放在构造器上调用的,而这个类的实例化只在web服务器启动时调用一次,那就是说loadResourceDefine方法只会调用一次,
* 如果资源和权限的对应关系在启动后发生了改变,那么缓存起来的权限数据就和实际授权数据不一致,那就会授权错误了。但如果资源和权限对应关系是不会改变的,这种方法性能会好很多。
* 要想解决 权限数据的一致性 可以直接在getAttributes方法里面调用数据库操作获取权限数据,通过被拦截url获取数据库中的所有权限,封装成Collection<ConfigAttribute>返回就行了。(灵活、简单
* 器启动加载顺序:1:调用loadResourceDefine()方法 2:调用supports()方法 3:调用getAllConfigAttributes()方法
*/
@Component
public class MyInvocationSecurityMetadataSourceService implements FilterInvocationSecurityMetadataSource {
@Autowired
MenuService menuService;
//spring工具类AntPathMatcher
AntPathMatcher antPathMatcher = new AntPathMatcher();
/**
* 参数是要访问的url,返回这个url对于的所有权限(或角色)
* 每次请求后台就会调用 得到请求所拥有的权限
* 这个方法在url请求时才会调用,服务器启动时不会执行这个方法
* getAttributes这个方法会根据你的请求路径去获取这个路径应该是有哪些权限才可以去访问。
*/
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
// object 是一个URL,被用户请求的url。
String requestUrl = ((FilterInvocation) o).getRequestUrl();
List<Menu> menus = menuService.getAllMenusWithRole();//获取所有的url路径与相应的权限
for (Menu menu : menus) {//循环已有的角色配置对象 进行url匹配
if (antPathMatcher.match(menu.getUrl(), requestUrl)) {// 路径支持Ant风格的通配符 /spitters/** boolean result = matcher.match(patternPath, requestPath);
List<Role> roles = menu.getRoles();//获取该url路径所需的权限
String[] str = new String[roles.size()];//创建相应数量的字符串数组
for (int i = 0; i < roles.size(); i++) {
str[i] = roles.get(i).getName();//将匹配的权限列表装入数组中
}
return SecurityConfig.createList(str);//封装访问相应的资源应该具有的权限
}
}
return SecurityConfig.createList("ROLE_LOGIN");//如果未匹配到相应的权限。则重新登录
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
4。配置中所调用的Service层
1_ 用户登录身份认证UserService
@Service
public class UserService implements UserDetailsService {
@Autowired
UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.findByUserName(username);
if (user == null) {
throw new UsernameNotFoundException("用户名不存在!");
}
user.setRoles(userMapper.getHrRolesById(user.getId()));//获取角色权限,并在实体类中封装权限信息
return user;
}
}
2_ MenuService
@Service
@CacheConfig(cacheNames = "menus_cache")
public class MenuService {
@Autowired
MenuMapper menuMapper;
//将url路径权限表缓存到redis数据库中
//@Cacheable 注解表示对该方法进行缓存,默认情况下,缓存的key是方法的参数,缓存的value是方法的返回值。当开发者在其他类中调用该方法时,
// 首先会根据调用参数查看缓存中是否有相关数据,若有,则直才妾使用缓存数据,该方法不会执行,否则执行该方法,执行成功后将返回值缓存起来,但若是在当前类中调用该方法,则缓存不会生效。
@Cacheable
public List<Menu> getAllMenusWithRole() {
return menuMapper.getAllMenusWithRole();
}
}
5。Mapper层
1_ UserMapper
public interface UserMapper {
//通过用户账号获取用户实体
User findByUserName(String username);
//通过用户id获取 用在户权限表中返回该用户的所有权限
List<Role> getHrRolesById(Integer id);
}
2_ MenuMapper
public interface MenuMapper {
//返回所有的url路径与相对应的权限
List<Menu> getAllMenusWithRole();
}
6。工具类
1_ 登录状态消息封装体RespBean
public class RespBean {
private Integer status;
private String msg;
private Object obj;
public static RespBean build() {
return new RespBean();
}
public static RespBean ok(String msg) {
return new RespBean(200, msg, null);
}
public static RespBean ok(String msg, Object obj) {
return new RespBean(200, msg, obj);
}
public static RespBean error(String msg) {
return new RespBean(500, msg, null);
}
public static RespBean error(String msg, Object obj) {
return new RespBean(500, msg, obj);
}
private RespBean() {
}
private RespBean(Integer status, String msg, Object obj) {
this.status = status;
this.msg = msg;
this.obj = obj;
}
public Integer getStatus() {
return status;
}
public RespBean setStatus(Integer status) {
this.status = status;
return this;
}
public String getMsg() {
return msg;
}
public RespBean setMsg(String msg) {
this.msg = msg;
return this;
}
public Object getObj() {
return obj;
}
public RespBean setObj(Object obj) {
this.obj = obj;
return this;
}
}
注释:这里是关键代码,未完善待续!