最近在实习中,需要用到Oauth协议来进行资源的认证,所以在此记录一下,让有需要的人少走一些弯路。(PS:Google真是个好东西,Github和官网也是好东西,虽然英文看起来挺费劲的)
这是官方实例:
认证服务器:https://github.com/spring-cloud-samples/authserver
SSO客户端:https://github.com/spring-cloud-samples/sso
要了解Oauth我们首先需要实现登录,这里使用SpringSecurity
刚接触Springboot和Security的人可能会比较难理解,所以我一步步来,编辑器使用的IDEA:
关于Spring Security 最好还是看官网说明
https://docs.spring.io/spring-security/site/docs/5.0.0.RELEASE/reference/htmlsingle/#pe-dpe
使用Security进行简单的密码登录验证
首先创建一个Springboot项目,引入Maven依赖
<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.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
我们新建一个Config文档用来放配置类
新建SecurityConfig,使其继承WebSecurityConfigurerAdapter,并使用@EnableWebSecurity 开启Spring Security
@Configuration
@EnableWebSecurity //开启Spring Security
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//加上这个方法就是不去管方法是否已被弃用
@SuppressWarnings("deprecation")
@Bean
public static NoOpPasswordEncoder passwordEncoder() {
return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin().permitAll()
.and()
.authorizeRequests().anyRequest().authenticated();
}
@Autowired
public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("haha").password("haha").roles("USER");
}
}
注意,如果你不使用别的passwordEncoder,要加上
@Bean
public static NoOpPasswordEncoder passwordEncoder() {
return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
}
因为密码的一般格式是:
{id}encodedPassword
这样的id是一个标识符,用于查找应使用哪个PasswordEncoder,并且encodedPassword是所选PasswordEncoder的原始编码密码。 该ID必须位于密码的开头,以{开始并以}结束。 如果找不到id,则id将为空。有不同的PasswordEncoder,如{bcrypt}和{sha256}等。
这样,就会使用默认的文本格式的passwordEncoder
否则因为没有提供明确的PasswordEncoder,将会出现下面的错误
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id “null”
在configure中配置的是具体配置,例如这里就是访问所有的接口都需要登录,你可以配置哪些不需要。也可以使用自定义的登录页面,这里我们使用Spring Security默认的登录页面即可。
然后我们增加一个接口
@RestController
public class UserController {
@RequestMapping(method = RequestMethod.GET)
public ResponseEntity add(){
return ResponseEntity.ok("hello");
}
}
访问http://localhost:8080 ,因为未登录,所以会跳转到http://localhost:8080/login
输入配置的账号密码后,跳转到输出hello的页面
当然,这种将账号密码写死的方式是不可能使用的,应使用数据库来存储这些用户信息
数据库使用MongoDB(Mysql也可以),关于JPA下的MongoDB可自行了解
在pom.xml中引入依赖
<!--springboot下的Mongdb依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
在application.yml中添加(默认是application.properties,但更推荐yml,树状式结构让配置更易看)
spring:
data:
mongodb:
database: auth
uri: mongodb://localhost:27017
文件结构如下,采取分层设计模式
首先创建一个实现了UserDetails的model类,或继承User也可以,根据需要改写重写的方法
@Document(collection="user")
public class User implements Serializable,UserDetails {
@Id
String id;
@Indexed(unique = true)
String username;
String password;
@CreatedDate
private Date createdAt;
@LastModifiedDate
private Date updatedAt;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
//可改为从权限表从获取权限
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
dao层只需实现MongoRepository接口即可,默认的CRUD会自动生成,需要额外的可以自己写sql语句或按其规定写方法
public interface UserRepository extends MongoRepository<User,String> {
public User findByUsername(String username);
}
还需要实现自定义的UserDetailsService
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user=userRepository.findByUsername(username);
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
System.out.println("haha");
authorities.add(new SimpleGrantedAuthority("ROLE_" + "ADMIN"));
org.springframework.security.core.userdetails.User userDetails=new org.springframework.security.core.userdetails.User(user.getUsername(),user.getPassword(),authorities);
return user;
}
}
这里我返回的Spring Security的User,你可以继承它来实现自己的额外需求,loadUserByUsername返回UserDetails,权限需要注意要加”ROLE_”前缀,Spring Security默认的角色前缀是”ROLE_”,使用hasRole方法时已经默认加上了
接下来在SecurityConfig中配置自定义的UserDetailsService
// register as a bean,否则在context中@Autowired非context管理的类会报null exception
@Bean
public UserDetailsService userDetailsService() {
return new CustomUserDetailsService();
}
@Autowired
public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService());
}
这个时候,就可以成功的从数据库中获取用户信息来进行登录验证了
我们使用Test来添加用户
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class AuthApplicationTests {
@Autowired
private UserRepository userRepository;
PasswordEncoder passwordEncoder=new BCryptPasswordEncoder();
@Test
public void addTest(){
User user=new User();
user.setUsername("xixi");
String password="xixi";
user.setPassword(passwordEncoder.encode(password));
userRepository.save(user);
}
}
可以看到生成的user中密码不是已明文的形式存储的了
再在SecurityConfig中配置PasswordEncoder
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Autowired
public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
auth.
userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
}
因为这里我们使用了BCryptPasswordEncoder,而不是使用明文的文本形式的NoOpPasswordEncoder,所以将下面去掉
@SuppressWarnings("deprecation")
@Bean
public static NoOpPasswordEncoder passwordEncoder() {
return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
}
再次登录,发现实现我们所要的目的了
如果前后端不分离,而你又希望使用自己的登录页面,这时只需要写一个Controller去处理GET方法的/login即可
@Controller
public class LoginLogoutController {
@RequestMapping("/login")
public String login(){
return "index";
}
}
并在SecurityConfig中配置
http
.formLogin().loginPage("/login").permitAll()
如果想让用户登录后跳转到首页,只需要在configure中配置即可
http
.formLogin().successForwardUrl("/success").permitAll();
添加一个Controller,用来跳转到登录成功所要到的页面
@RestController
public class LoginLogoutController {
@RequestMapping(value = "/success",method = RequestMethod.POST)
public String success(){
return "success";
}
}
注意,默认接受POST方法