SpringSecurity
SpringSecurity完成基于数据库的认证(登录)
application.yml配置
1
2
3
4
5
6
7
8
9
10spring:
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.Drivermysql,mybatis,druid依赖必须有
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<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()方法,用于返回用户所有角色
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88public 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;
}
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (Role role : roles) {
authorities.add(new SimpleGrantedAuthority("ROLE_"+role.getName()));
}
return authorities;
}
public String getPassword() {
return password;
}
public String getUsername() {
return username;
}
public boolean isAccountNonExpired() { //账户是否未过期
return true;//一般表里会有一个字段表示账户是否过期,但是我们的表里还没有,为了方便起见,默认返回true,也就是未过期
}
public boolean isAccountNonLocked() {//账户是否未锁定
return !locked;
}
public boolean isCredentialsNonExpired() {//密码是否未过期
return true;
}
public boolean isEnabled() {//是否可用
return enabled;
}
}
Role类,List< Role > roles;变量用于存储user的角色属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43package 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这个方法)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class UserService implements UserDetailsService {
UserMapper userMapper;
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中定义方法
1
2
3
4
5
6
7
8
9
public interface UserMapper {
User loadUserByUsername(String username);
List<Role> getUserRolesById(Integer id);
}在UserMapper.xml中实现
1
2
3
4
5
6
7
8
9
10<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)方法来定义权限的访问范围
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class SecurityConfig extends WebSecurityConfigurerAdapter {
UserService userService;
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService);//指定使用自定义查询用户信息来完成身份认证
}
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
String hierarchy = "ROLE_dba > ROLE_admin \n ROLE_admin > ROLE_user";
roleHierarchy.setHierarchy(hierarchy);
return roleHierarchy;
}
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的事情。==角色的继承==
1
2
3
4
5
6
7
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注入进来
以下是关键代码以及解释:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 根据请求的地址,分析出来这个地址需要哪些角色
* 根据需要的角色,拿出来目前“我”具有的角色,比较一下是否具备
*/
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注解
以下是关键方法以及代码的解释
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 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”
1
2
3
4
5
6
7
class Oauth2ApplicationTests {
public void contextLoads() {
System.out.println(new BCryptPasswordEncoder().encode("123"));
}
}配置授权服务器AuthorizationServerConfig继承AuthorizationServerConfigurerAdapter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
AuthenticationManager authenticationManager;//主要用来支持password的认证模式
RedisConnectionFactory redisConnectionFactory;
UserDetailsService userDetailsService;//刷新token的时候会用到
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
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");//一会需要的密码
}
/**
* 配置令牌的存储,待会把令牌存到哪去!
*/
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(new RedisTokenStore(redisConnectionFactory))
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
}
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients();//表示支持登录认证
}
}配置资源服务器ResourceServerConfig继承ResourceServerConfigurerAdapter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23/**
* 资源服务器
*/
//表示是个配置类
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId("rid")//指定资源id,就是在授权服务器里面配置的 rid 【.resourceIds("rid")】
.stateless(true);//意思是这些资源是基于令牌来认证的
}
/**
* 这就是我提供的资源!
*/
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/admin/**").hasRole("admin")
.antMatchers("/user/**").hasRole("user")
.anyRequest().authenticated();//剩下其他的请求都是(authenticated)登录之后就可以访问
}
}配置SecurityConfig继承WebSecurityConfigurerAdapter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class SecurityConfig extends WebSecurityConfigurerAdapter {
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();//这个authenticationManager和
// 下面的userDetailsService会传给授权服务器
}
protected UserDetailsService userDetailsService() {
return super.userDetailsService();
}
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");
}
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/oauth/**")
.authorizeRequests()
.antMatchers("/oauth/**").permitAll()
.and().csrf().disable();
}
}在application.properties里配置
1
2
3
4spring.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方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45public class MyAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class SecurityConfig extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated()
.and()
.formLogin().permitAll()
.and().csrf().disable();
http.addFilterAt(myAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);//加一个filter
}
MyAuthenticationFilter myAuthenticationFilter() throws Exception {
MyAuthenticationFilter filter = new MyAuthenticationFilter();
filter.setAuthenticationManager(authenticationManagerBean());
return filter;
}
}