SpringSecurity
SpringSecurity完成基于数据库的认证(登录)
application.yml配置
spring: datasource: name: test url: jdbc:mysql://localhost:3306/javaboy?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai username: root password: wqeq # 使用druid数据源 type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver
mysql,mybatis,druid依赖必须有
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.7</version> </dependency>
创建实体类
User类 :
实现 UserDetails接口,并实现其中的方法(这是个规范,因为每个人设计的用户名和密码不一定都是username和password)
List< Role > roles;变量用于存储user的角色属性
Collection<? enxtend GrandtedAuthority> getAuthorities()方法,用于返回用户所有角色
public class User implements UserDetails { private Integer id; private String username; private String password; private Boolean enabled; private Boolean locked; private List<Role> roles; public Boolean getLocked() { return locked; } 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 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 void setLocked(Boolean locked) { this.locked = locked; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { List<SimpleGrantedAuthority> authorities = new ArrayList<>(); for (Role role : roles) { authorities.add(new SimpleGrantedAuthority("ROLE_"+role.getName())); } return authorities; } @Override public String getPassword() { return password; } @Override public String getUsername() { return username; } @Override public boolean isAccountNonExpired() { //账户是否未过期 return true;//一般表里会有一个字段表示账户是否过期,但是我们的表里还没有,为了方便起见,默认返回true,也就是未过期 } @Override public boolean isAccountNonLocked() {//账户是否未锁定 return !locked; } @Override public boolean isCredentialsNonExpired() {//密码是否未过期 return true; } @Override public boolean isEnabled() {//是否可用 return enabled; } }
Role类,List< Role > roles;变量用于存储user的角色属性
package com.lcdzzz.mysecuritydb.bean; import java.util.List; public class Role { private Integer id; private String name; private String nameZh; 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 getName() { return name; } public void setName(String name) { this.name = name; } public String getNameZh() { return nameZh; } public void setNameZh(String nameZh) { this.nameZh = nameZh; } }
定义mapper层和实现类(service层)
UserMapper
创建service层中的UserService类,实现UserDetailService这个类和它的方法(public UserDetails loadUserByUsername这个方法)
@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.getUserRolesById(user.getId())); return user; } }
用@Autowired注解注入UserMapper对象
在loadUserByUsername来自定义查询用户信息的方法
- 一个是loadUserByUsername这个方法,如果没有在数据库找到符合条件的数据,则返回null,抛出UsernameNotFoundException异常
- 一个是getRolesById这个方法,得到用户的角色(身份)信息
在UserMapper中定义方法
@Mapper public interface UserMapper { User loadUserByUsername(String username); List<Role> getUserRolesById(Integer id); }
在UserMapper.xml中实现
<mapper namespace="com.lcdzzz.mysecuritydb.mapper.UserMapper"> <select id="loadUserByUsername" resultType="com.lcdzzz.mysecuritydb.bean.User"> select * from user where username=#{username} </select> <select id="getUserRolesById" resultType="com.lcdzzz.mysecuritydb.bean.Role"> select * from role where id in (select rid from user_role where uid=#{id}) </select> </mapper>
config层中的SecurityConfig
记得加上@Configuration注解
SecurityConfig继承WebSecurityConfigurerAdapter
来一个protect void configure(AuthenticationManagerBuilder auth) 方法
用@Autowired注解把UserService注入进来
来一个PasswordEncoder passwordEncoder()方法,再return new BCryptPasswordEncoder(),最后加上注解@Bean
在“3”说的方法里写:auth.userDetailService(userService) 来指定使用自定义查询用户信息来完成身份认证;
并且通过protected void configure(HttpSecurity http)方法来定义权限的访问范围
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired UserService userService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userService);//指定使用自定义查询用户信息来完成身份认证 } @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(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/dba/**").hasRole("dba") .antMatchers("/admin/**").hasRole("admin") .antMatchers("/user/**").hasRole("user") .anyRequest().authenticated() .and() .formLogin() .permitAll() .and() .csrf().disable(); } }
其中这段代码很重要他代表dba既可以干admin的事情也可以干user的事情;admin可以干user的事情。==角色的继承==
@Bean RoleHierarchy roleHierarchy() { RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl(); String hierarchy = "ROLE_dba > ROLE_admin \n ROLE_admin > ROLE_user"; roleHierarchy.setHierarchy(hierarchy); return roleHierarchy; }
在controller层写一个HelloController
在porm.xml里配一个resources,指定资源文件目录
动态配置权限(基于数据库)
上面登录完成后,要想通过数据库来动态配置权限。就要定义几个东西
关于MyFilter类:
在config层中创一个 MyFilter类,当然名字是自定义的。去实现 FilterInvocationSecurityMetadataSource并实现其中的三个方法
getAttribute方法的作用:==根据请求的地址,分析出来这个地址需要哪些角色==
AntPathMacher pathMatcher = new AntPathMatcher()
这个是一个路径匹配符在这个方法正式运作之前,要在Menu定义一个
private List< Role> roles
变量,因为每个menu需要是某个角色才能访问,意思是当前这个menu需要具备哪些角色才能访问在mapper层来一个MenuMapper类,定义
List< Menu> getAllMenus()
方法在service层来一个 MenuService ,实现一个
List< Menu> getAllMenu()
方法,通过@Autowired把MenuMapper自动装配过来在MenuMapper中实现。在这里 因为到时候查出来是一对多的关系,所以不能resultType,要使用resultMap
转到MyFilter,用@Autowired把MenuService注入进来
以下是关键代码以及解释:
@Override /** * 根据请求的地址,分析出来这个地址需要哪些角色 * 根据需要的角色,拿出来目前“我”具有的角色,比较一下是否具备 */ public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException { 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");//如果角色是ROLE_login,代表登录之后就可以访问这个资源 }
关于MyAccessDecisionManager
创建MyAccessDecisionManager类,让它实现AccessDecisionManager接口,并实现里面的方法,记得在类的上面加上@Component注解
以下是关键方法以及代码的解释
@Override /** * authentication保存着当前登录的用户的信息。从这里可以知道我有哪些角色 * collection就是MyFilter类中的getAttributes的返回值。从这里可以知道需要哪些角色 */ 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) {//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("非法请求!");//非常不幸的走到了这一步,意味着你是非法请求(不然中途就break了) }
Spring Security结合OAuth2协议
生成一个加密后的密码,明文“123”
@SpringBootTest class Oauth2ApplicationTests { @Test public void contextLoads() { System.out.println(new BCryptPasswordEncoder().encode("123")); } }
配置授权服务器AuthorizationServerConfig继承AuthorizationServerConfigurerAdapter
@Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired AuthenticationManager authenticationManager;//主要用来支持password的认证模式 @Autowired RedisConnectionFactory redisConnectionFactory; @Autowired UserDetailsService userDetailsService;//刷新token的时候会用到 @Bean PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory()//配置在内存里边的 .withClient("password")//认证模式为password模式 .authorizedGrantTypes("password", "refresh_token")//配授权模式,两种 .accessTokenValiditySeconds(1800)//token的过期时间,1800秒 .resourceIds("rid")//给资源取个名字 .scopes("all") .secret("c405d914-f9cf-42d5-972e-0ffd4a1522bd");//一会需要的密码 } /** * 配置令牌的存储,待会把令牌存到哪去! */ @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.tokenStore(new RedisTokenStore(redisConnectionFactory)) .authenticationManager(authenticationManager) .userDetailsService(userDetailsService); } @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security.allowFormAuthenticationForClients();//表示支持登录认证 } }
配置资源服务器ResourceServerConfig继承ResourceServerConfigurerAdapter
/** * 资源服务器 */ @Configuration //表示是个配置类 @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources.resourceId("rid")//指定资源id,就是在授权服务器里面配置的 rid 【.resourceIds("rid")】 .stateless(true);//意思是这些资源是基于令牌来认证的 } /** * 这就是我提供的资源! */ @Override public void configure(HttpSecurity http) throws Exception { http.authorizeRequests().antMatchers("/admin/**").hasRole("admin") .antMatchers("/user/**").hasRole("user") .anyRequest().authenticated();//剩下其他的请求都是(authenticated)登录之后就可以访问 } }
配置SecurityConfig继承WebSecurityConfigurerAdapter
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override @Bean protected AuthenticationManager authenticationManager() throws Exception { return super.authenticationManager();//这个authenticationManager和 // 下面的userDetailsService会传给授权服务器 } @Bean @Override protected UserDetailsService userDetailsService() { return super.userDetailsService(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("lcdzzz").password("$2a$10$BQsi4LxO/9536a2wwW.5D.T/t3fm52xzF17Eo6xlFinxuk8uKjEg2").roles("admin") .and() .withUser("zhoudian") .password("$2a$10$BQsi4LxO/9536a2wwW.5D.T/t3fm52xzF17Eo6xlFinxuk8uKjEg2") .roles("user"); } @Override protected void configure(HttpSecurity http) throws Exception { http.antMatcher("/oauth/**") .authorizeRequests() .antMatchers("/oauth/**").permitAll() .and().csrf().disable(); } }
在application.properties里配置
spring.redis.host=8.142.93.194 spring.redis.port=6379 spring.redis.database=0 spring.redis.timeout=1000
Spring Security使用Json登录
在filter层中创建MyAuthenticationFilter来继承UsernamePasswordAuthenticationFilter,重写父类的attemptAuthentication方法
public class MyAuthenticationFilter extends UsernamePasswordAuthenticationFilter { @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (!request.getMethod().equals("POST")) { throw new AuthenticationServiceException( "Authentication method not supported: " + request.getMethod()); } if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) { //说明用户以 JSON 的形式传递的参数 String username = null; String password = null; try { Map<String, String> map = new ObjectMapper().readValue(request.getInputStream(), Map.class);//getInputStream是一个流,把这个流解析出来就是个json字符串了 //不是所有请求都有流,get就没有,只有有body的请求才有流 username = map.get("username"); password = map.get("password"); } catch (IOException e) { e.printStackTrace(); } if (username == null) { username = ""; } if (password == null) { password = ""; } username = username.trim(); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); // Allow subclasses to set the "details" property setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } return super.attemptAuthentication(request, response); } }
接下来如何让上面的东西生效呢?则需要再配下security的配置
在config层创建一个SecurityConfig继承WebSecurityConfigurerAdapter
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().anyRequest().authenticated() .and() .formLogin().permitAll() .and().csrf().disable(); http.addFilterAt(myAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);//加一个filter } @Bean MyAuthenticationFilter myAuthenticationFilter() throws Exception { MyAuthenticationFilter filter = new MyAuthenticationFilter(); filter.setAuthenticationManager(authenticationManagerBean()); return filter; } }